dev #2

Merged
Exverge merged 21 commits from :dev into dev 2024-03-23 16:52:48 +01:00
33 changed files with 1389 additions and 95 deletions

View file

@ -1,22 +1,14 @@
# create-svelte
# Suyu website
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
This project contains the source code for the Suyu website, found at [suyu.dev](https://suyu.dev)
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
If you are deeloping, please take note of the `.env.example` & the secrets config (found at src/lib/server/secrets/secrets.example.json).
At minimum, please make sure to clone the `secrets.example.json` file and rename it to `secrets.json`. Otherwise, the project will not run or build (you don't have to edit the values to get it running, but you can if you'd like).
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
Once you've installed dependencies with `npm install` (or `pnpm install` or `yarn`), you can start a development server by running:
```bash
npm run dev
@ -27,12 +19,9 @@ npm run dev -- --open
## Building
To create a production version of your app:
To create a production version of our app, you can run:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View file

@ -41,6 +41,22 @@ h3 {
@apply outline-none ring-2 ring-sky-400;
}
::-webkit-scrollbar {
@apply w-[8px];
}
::-webkit-scrollbar-track {
@apply bg-[var(--page-bg)]
}
::-webkit-scrollbar-thumb {
@apply rounded-xl bg-[#3c4f7c]
}
::-webkit-scrollbar-thumb:hover {
@apply bg-[#526ca8]
}
.cta-button {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.06)),
radial-gradient(109.26% 109.26% at 49.83% 13.37%, #ffffff 0%, #babaca 100%);

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
src/assets/fonts/suivar.ttf Normal file

Binary file not shown.

View file

@ -0,0 +1,78 @@
<script lang="ts">
export let compatibility: "goated" | "based" | "cringe";
export let image: string;
export let title: string;
export let releaseYear: number;
function capitalizeFirstLetter(string: typeof compatibility) {
if (string === "goated") {
return "Good";
} else if (string === "based") {
return "Okay";
} else if (string === "cringe") {
return "Bad";
}
}
</script>
<div class="card">
<div class="card-image">
<img src={image || ""} alt="Mario Odyssey" />
</div>
<div class="info">
<h3 class="header">{title}</h3>
<div class="content">Released: {releaseYear}</div>
<div>
Compatibility: <span class={compatibility}>{capitalizeFirstLetter(compatibility)}</span>
</div>
</div>
</div>
<style>
.card {
aspect-ratio: 1.5/2;
width: fit-content;
height: 450px;
background-color: rgb(47, 57, 76);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 0 24px rgb(32, 33, 45);
border: solid thin rgb(101, 109, 132);
}
.card-image {
border-radius: 4px;
overflow: hidden;
width: 100%;
height: 100%;
border: solid thin rgb(101, 109, 132);
}
.card-image > img {
object-fit: cover;
width: 100%;
height: 100%;
}
.header {
font-size: 28px;
}
.goated {
color: #78dca0;
text-shadow: 0px 0px 8px #78dca0;
}
.based {
color: #eab876;
text-shadow: 0px 0px 8px #eab876;
}
.cringe {
color: #ff3a3a;
text-shadow: 0px 0px 8px #ff3a3a;
}
</style>

View file

@ -0,0 +1,283 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import Card from "$components/Card.svelte";
import type { ICard } from "$lib/util/types";
export let cards: ICard[];
let selectedCard = 0;
let instantSelectedCard = 0;
let animating = false;
async function go(dir: number) {
if (dir > 0) {
cardScroll({
deltaY: 100,
shiftKey: true,
preventDefault: () => {},
} as any);
} else {
cardScroll({
deltaY: -100,
shiftKey: true,
preventDefault: () => {},
} as any);
}
}
onMount(() => {
const key = (e: KeyboardEvent) => {
// right arrow, run cardScroll with positive
if (e.key === "ArrowRight") {
e.preventDefault();
go(1);
}
if (e.key === "ArrowLeft") {
e.preventDefault();
go(-1);
}
};
window.addEventListener("keydown", key);
return () => {
window.removeEventListener("keydown", key);
};
});
async function cardScroll(e: WheelEvent) {
if (!e.shiftKey || window.innerWidth < 560) return;
e.preventDefault();
const animations: Animation[] = [];
const duration = 500;
const easing = "cubic-bezier(.29,1.03,.5,1)";
if (animating) return;
animating = true;
if (e.deltaY > 0) {
instantSelectedCard = (selectedCard + 1) % cards.length;
} else {
instantSelectedCard = (selectedCard - 1 + cards.length) % cards.length;
}
const cardLeft = document.querySelector(".card-3d.left") as HTMLElement;
const cardRight = document.querySelector(".card-3d.right") as HTMLElement;
const cardCenter = document.querySelector(
".card-3d:not(.left):not(.right):not(.transition-left):not(.transition-right)",
) as HTMLElement;
const cardTransitionLeft = document.querySelector(
".card-3d.transition-left",
) as HTMLElement;
const cardTransitionRight = document.querySelector(
".card-3d.transition-right",
) as HTMLElement;
cardTransitionLeft.style.display = "block";
cardTransitionRight.style.display = "block";
setTimeout(async () => {
selectedCard = instantSelectedCard;
await tick();
cardTransitionLeft.style.display = "none";
cardTransitionRight.style.display = "none";
animations.forEach((anim) => anim.cancel());
setTimeout(() => {
animating = false;
}, 10);
}, duration);
const cardLeftBounds = cardLeft.getBoundingClientRect();
const cardRightBounds = cardRight.getBoundingClientRect();
const cardCenterBounds = cardCenter.getBoundingClientRect();
if (e.deltaY > 0) {
animations.push(
cardRight.animate(
[
{
transform: `translateX(${cardCenterBounds.left - cardRightBounds.left + 62}px)`,
},
],
{
duration,
fill: "forwards",
easing,
},
),
cardCenter.animate(
[
{
transform: `translateX(${cardLeftBounds.left - cardCenterBounds.left - 83}px) perspective(1000px) translateZ(-150px) rotateY(-50deg)`,
},
],
{
duration,
fill: "forwards",
easing,
},
),
cardLeft.animate(
[
{
opacity: 0,
transform:
"perspective(1000px) translateZ(-150px) rotateY(-80deg) translateX(-400px)",
},
],
{
duration,
fill: "forwards",
easing,
},
),
cardTransitionLeft.animate(
[
{
transform:
"translateX(1150px) perspective(1000px) translateZ(-400px) rotateY(80deg)",
opacity: 0,
},
{
transform:
"translateX(1013px) perspective(1000px) translateZ(-150px) rotateY(50deg)",
opacity: 1,
},
],
{
duration,
fill: "forwards",
easing,
},
),
);
} else {
animations.push(
cardLeft.animate(
[
{
transform: `translateX(${cardCenterBounds.left - cardLeftBounds.left + 83}px)`,
},
],
{
duration,
fill: "forwards",
easing,
},
),
cardCenter.animate(
[
{
transform: `translateX(${cardRightBounds.left - cardCenterBounds.left - 62}px) perspective(1000px) translateZ(-150px) rotateY(50deg)`,
},
],
{
duration,
fill: "forwards",
easing,
},
),
cardRight.animate(
[
{
opacity: 0,
transform:
"perspective(1000px) translateZ(-150px) rotateY(80deg) translateX(400px)",
},
],
{
duration,
fill: "forwards",
easing,
},
),
cardTransitionRight.animate(
[
{
transform:
"translateX(-1150px) perspective(1000px) translateZ(-400px) rotateY(-80deg)",
opacity: 0,
},
{
transform:
"translateX(-1013px) perspective(1000px) translateZ(-150px) rotateY(-50deg)",
opacity: 1,
},
],
{
duration,
fill: "forwards",
easing,
},
),
);
}
}
</script>
<!-- look, i don't hate the disabled, but this
site's concept is pretty inaccessible as it
is (and i just so happen to be partially blind
in an eye, so), also we do have keyboard events
we just register them through onMount() so fuck
you a11y -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cards" on:wheel={cardScroll}>
<div class="card-3d transition-left">
<Card {...cards[(instantSelectedCard + 1) % cards.length]} />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="card-3d left" on:click={() => go(-1)}>
<Card {...cards[selectedCard - 1 < 0 ? cards.length - 1 : selectedCard - 1]} />
</div>
<div class="card-3d">
<Card {...cards[selectedCard]} />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="card-3d right" on:click={() => go(1)}>
<Card {...cards[(selectedCard + 1) % cards.length]} />
</div>
<div class="card-3d transition-right">
<Card {...cards[(instantSelectedCard + 2) % cards.length]} />
</div>
</div>
<style>
.cards {
display: flex;
max-width: 100%;
}
.card-3d {
z-index: 3;
}
.card-3d.left {
transform: perspective(1000px) translateZ(-150px) rotateY(-50deg);
z-index: 2;
}
.card-3d.right {
transform: perspective(1000px) translateZ(-150px) rotateY(50deg);
z-index: 2;
}
.card-3d {
position: relative;
}
.card-3d.transition-left,
.card-3d.transition-right {
opacity: 0;
z-index: 1;
display: none;
}
@media (max-width: 560px) {
.card-3d {
transform: none !important;
}
.cards {
flex-direction: column;
gap: 24px;
}
}
</style>

View file

@ -0,0 +1,21 @@
<script lang="ts">
export let count: number = 0;
export let subText: string = '';
</script>
{#if count > 0}
<div class="flex flex-col gap-0">
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
<!-- <AnimatedCounter
values={Array.from({ length: contributors + 1 }, (_, i) => i.toString())}
startImmediately={false}
direction="up"
loop={false}
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
initialValue={(contributors - 1).toString()}
/>+ -->
{count}+
</h2>
<div class="text-[#A6A5A7]">{subText}</div>
</div>
{/if}

View file

@ -52,7 +52,7 @@
{player.nickname}{#if player !== room.players[room.players.length - 1]},{" "}
{/if}
{/each}
{/if} | {room.hasPassword ? "Private" : "Public"} | {room.address}:{room.port}
{/if} | {room.hasPassword ? "Private" : "Public"}
</div>
</div>
</div>

View file

@ -0,0 +1,5 @@
{
"landingHeader": "suyu",
"landingOne": "Suyu is an open-source, Switch compatible emulator with almost full coverage of the game library.",
"landingCardHeader": "We care about preservation"
}

1
src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -4,7 +4,7 @@ import type { SuyuUser } from "../schema";
import { v4 } from "uuid";
export class RoomManager {
private static rooms: Room[] = [];
static rooms: Room[] = [];
static roomTimeout: Record<string, number> = {}; // room id, last heard from
static createRoom(room: IRoomConfig) {
const existingRoom = this.rooms.find((r) => r.host.username === room.host.username);

View file

@ -21,13 +21,22 @@ export async function useAuth(
const decoded: IJwtData = jwt.verify(token, Buffer.from(PUBLIC_KEY), {
algorithms: ["RS256"],
}) as IJwtData;
const user = await userRepo.findOne({
let user = await userRepo.findOne({
where: {
apiKey: decoded.apiKey,
},
loadEagerRelations: eager || false,
relations: eager ? ["sentFriendRequests", "receivedFriendRequests"] : [],
});
if (!user) {
user = await userRepo.findOne({
where: {
id: decoded.id,
},
loadEagerRelations: eager || false,
relations: eager ? ["sentFriendRequests", "receivedFriendRequests"] : [],
});
}
return user;
}
const user = await userRepo.findOne({

View file

@ -142,15 +142,15 @@
},
{
name: "FAQ",
href: "/coming-soon",
href: "/faq",
},
{
name: "Discord",
href: "https://discord.gg/suyu",
},
{
name: "GitLab",
href: "https://gitlab.com/suyu-emu/suyu",
name: "Git",
href: "https://git.suyu.dev/suyu/suyu",
},
// {
// name: $token || data.tokenCookie ? "Account" : "Sign up",
@ -290,13 +290,13 @@
>
<a href="/coming-soon" class="px-5 py-3 transition hover:text-white">Blog</a>
<a href="/coming-soon" class="px-5 py-3 transition hover:text-white">Docs</a>
<a href="/coming-soon" class="px-5 py-3 transition hover:text-white">FAQ</a>
<a href="/faq" class="px-5 py-3 transition hover:text-white">FAQ</a>
</div>
<div class="flex w-full flex-row items-center justify-end text-[#A6A5A7]">
<div class="flex flex-row gap-4 max-[625px]:hidden">
<a
class="p-2 transition hover:text-white"
href="https://gitlab.com/suyu-emu/suyu"
href="https://git.suyu.dev/suyu/suyu"
rel="noreferrer noopener"
target="_blank"
>

View file

@ -2,6 +2,7 @@
import embedImage from "$assets/branding/suyu__Embed-Image.png";
import type { PageData } from "./$types";
import suyuWindow from "$assets/mockups/suyuwindow.png";
import HomepageCounter from "$components/HomepageCounter.svelte";
import { XCircleOutline } from "flowbite-svelte-icons";
import { Dialog } from "radix-svelte";
@ -70,18 +71,15 @@
</p>
<div class="flex flex-col gap-4 md:flex-row">
<a
href="https://gitlab.com/suyu-emu/suyu/-/releases"
target="_blank"
href="/download"
rel="noreferrer noopener"
class="cta-button"
>
Download <svg
class=""
style="--icon-color:#000"
width="16"
height="16"
viewBox="0 0 16 16"
fill="#000"
fill="currentColor"
role="img"
focusable="false"
aria-hidden="true"
@ -91,18 +89,16 @@
>
</a>
<a
href="https://gitlab.com/suyu-emu/suyu"
href="https://git.suyu.dev/suyu/suyu"
target="_blank"
rel="noreferrer noopener"
class="button text-[#8A8F98]"
>
Contribute <svg
class=""
style="--icon-color:#8A8F98"
width="16"
height="16"
viewBox="0 0 16 16"
fill="#8A8F98"
fill="currentColor"
role="img"
focusable="false"
aria-hidden="true"
@ -119,60 +115,10 @@
class="flex w-full flex-shrink-0 flex-col gap-8 rounded-b-[2.25rem] bg-[#110d10] p-12 lg:w-[35%]"
>
<h1 class="text-[48px] leading-[0.9]">By the numbers</h1>
<div class="flex flex-col gap-0">
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
<!-- <AnimatedCounter
values={Array.from({ length: contributors + 1 }, (_, i) => i.toString())}
startImmediately={false}
direction="up"
loop={false}
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
initialValue={(contributors - 1).toString()}
/>+ -->
{contributors}+
</h2>
<div class="text-[#A6A5A7]">dedicated contributors</div>
</div>
<div class="flex flex-col gap-0">
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
{starCount}+
</h2>
<div class="text-[#A6A5A7]">GitLab stars</div>
</div>
<div class="flex flex-col gap-0">
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
<!-- <AnimatedCounter
values={// array from 0 - 4000 with steps of 100
Array.from({ length: 41 }, (_, i) => (i * 100).toString())}
startImmediately={false}
direction="up"
loop={false}
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
initialValue={"3900"}
/>+ -->
4000+
</h2>
<div class="text-[#A6A5A7]">supported games</div>
</div>
<div class="flex flex-col gap-0">
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
<!-- <AnimatedCounter
values={Array.from(
{
length: memberCount / 100 + 1,
},
(_, i) => (i * 100).toString(),
)}
startImmediately={false}
direction="up"
loop={false}
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
initialValue={(memberCount - 100).toString()}
/>+ -->
{memberCount}+
</h2>
<div class="text-[#A6A5A7]">members on Discord</div>
</div>
<HomepageCounter count={contributors} subText="dedicated contributors" />
<HomepageCounter count={starCount} subText="GitLab stars" />
<HomepageCounter count={4000} subText="supported games" />
<HomepageCounter count={memberCount} subText="members on Discord" />
</div>
<div class="flex w-full flex-1 rounded-[2.25rem] bg-[#110d10] lg:rounded-tl-none">
<div
@ -233,14 +179,14 @@
</svg>
</a>
<a
href="https://gitlab.com/suyu-emu/suyu"
href="https://git.suyu.dev/suyu/suyu"
target="_blank"
rel="noreferrer noopener"
class="relative w-full rounded-[2.25rem] bg-[#f78c40] p-12 text-black"
>
<h2 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">GitLab</h2>
<h2 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">Git</h2>
<p class="mt-2 text-lg leading-relaxed">
GitLab is where all the magic of suyu happens. We're always looking for new contributors
Our Git instance is where all the magic of suyu happens. We're always looking for new contributors
to help us out, so feel free to check out our code.
</p>
<svg

View file

@ -3,6 +3,6 @@ import { RoomManager } from "$lib/server/class/Room.js";
export function load({ request }) {
const rooms = RoomManager.getRooms().map((r) => r.toJSON()) || [];
return {
rooms: rooms,
rooms: rooms.reverse(),
};
}

View file

@ -0,0 +1,129 @@
<script lang="ts">
import { onMount } from "svelte";
onMount(async () => {
const UA = navigator.userAgent;
const url = `https://gitlab.com/api/v4/projects/55919530/repository/tree`;
async function getTag() {
try {
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
});
// Convert to JSON
let files = await response.json();
files = files.filter((f) => f.name.startsWith("v") && f.type === "tree");
// get the latest release using the version number
// thanks, copilot!!
const latestRelease = files.reduce((a, b) => {
const aVersion = a.name.replace("v", "").split(".");
const bVersion = b.name.replace("v", "").split(".");
if (aVersion[0] > bVersion[0]) {
return a;
} else if (aVersion[0] < bVersion[0]) {
return b;
} else {
if (aVersion[1] > bVersion[1]) {
return a;
} else if (aVersion[1] < bVersion[1]) {
return b;
} else {
if (aVersion[2] > bVersion[2]) {
return a;
} else if (aVersion[2] < bVersion[2]) {
return b;
} else {
return a;
}
}
}
});
// Release found
if (latestRelease) {
console.log("Latest release tag:", latestRelease.name);
return latestRelease.name; // Assuming the first result is the latest
} else {
console.log("No releases found.");
return null;
}
} catch (error) {
console.error("Error fetching latest release tag:", error);
return null;
}
}
const latestRelease = await getTag();
setTimeout(() => {
if (UA.includes("Windows")) {
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/raw/master/${latestRelease}/Suyu-Windows_x64.7z`;
} else if (UA.includes("Linux")) {
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/raw/master/${latestRelease}/suyu-mainline--.AppImage`;
} else if (UA.includes("Macintosh;")) {
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/raw/master/${latestRelease}/suyu-macOS-arm64.dmg?inline=false`;
} else {
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/blob/master/${latestRelease}/`;
}
}, 3000);
})
</script>
<svelte:head>
<title>Downloading Suyu</title>
</svelte:head>
<div
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-[#110d10] p-8 md:p-12"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="525"
viewBox="0 0 512 525"
fill="none"
style="animation-duration: 300s; transform-origin: 50% 50%; animation-iteration-count: infinite; animation-timing-function: linear; animation-name: spin; animation-delay: 0s; animation-direction: normal; animation-fill-mode: none; animation-play-state: running;"
class="pointer-events-none absolute -bottom-[18rem] right-0 z-0 animate-spin opacity-20"
>
<path
d="M511.5 262.12C511.5 353.613 465.547 434.182 396.019 480.947C408.179 457.937 415.083 431.597 415.083 403.617C415.083 313.723 343.816 240.744 255.992 240.744C191.257 240.744 138.692 186.941 138.692 120.622C138.692 54.3027 191.257 0.5 255.992 0.5C397.026 0.5 511.5 117.695 511.5 262.12ZM255.992 53.5225C243.745 53.5225 233.816 63.7047 233.816 76.2224C233.816 88.7388 243.745 98.9223 255.992 98.9223C268.257 98.9223 278.173 88.7387 278.173 76.2224C278.173 63.7048 268.257 53.5225 255.992 53.5225ZM299.355 97.9223C287.104 97.9223 277.173 108.104 277.173 120.622C277.173 133.139 287.104 143.322 299.355 143.322C311.62 143.322 321.536 133.139 321.536 120.622C321.536 108.104 311.62 97.9223 299.355 97.9223ZM212.635 97.9223C200.382 97.9223 190.455 108.104 190.455 120.622C190.455 133.139 200.382 143.322 212.635 143.322C224.889 143.322 234.816 133.139 234.816 120.622C234.816 108.104 224.888 97.9223 212.635 97.9223ZM255.992 142.322C243.745 142.322 233.816 152.505 233.816 165.021C233.816 177.539 243.745 187.721 255.992 187.721C268.257 187.721 278.173 177.538 278.173 165.021C278.173 152.505 268.257 142.322 255.992 142.322Z"
stroke="white"
/>
<path
d="M0.5 262.119C0.5 170.626 46.444 90.0553 115.976 43.2909C103.82 66.3019 96.9172 92.6424 96.9172 120.622C96.9172 210.516 168.174 283.495 255.992 283.495C320.735 283.495 373.305 337.298 373.305 403.617C373.305 469.934 320.735 523.739 255.992 523.739C114.974 523.739 0.5 406.544 0.5 262.119ZM255.992 336.517C243.744 336.517 233.816 346.7 233.816 359.217C233.816 371.735 243.745 381.917 255.992 381.917C268.256 381.917 278.173 371.735 278.173 359.217C278.173 346.701 268.256 336.517 255.992 336.517ZM299.355 380.917C287.104 380.917 277.173 391.099 277.173 403.617C277.173 416.135 287.104 426.317 299.355 426.317C311.619 426.317 321.536 416.135 321.536 403.617C321.536 391.099 311.619 380.917 299.355 380.917ZM255.992 425.317C243.745 425.317 233.816 435.499 233.816 448.016C233.816 460.533 243.744 470.717 255.992 470.717C268.256 470.717 278.173 460.533 278.173 448.016C278.173 435.499 268.256 425.317 255.992 425.317ZM212.634 380.917C200.382 380.917 190.454 391.099 190.454 403.617C190.454 416.135 200.382 426.317 212.634 426.317C224.888 426.317 234.816 416.135 234.816 403.617C234.816 391.099 224.888 380.917 212.634 380.917Z"
stroke="white"
/>
</svg>
<script>
onMount(() => {
let text = "Downloading Suyu";
let interval = setInterval(() => {
text += ".";
if (text.length > 16) {
text = "Downloading Suyu";
}
$text = text;
}, 500);
$: clearInterval(interval);
});
</script>
<!-- TODO: Have the 3 dots loop (e.g . -> .. -> ...) -->
<h1 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
Downloading Suyu...
</h1>
<!-- <h1 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
{#if $text}
{$text}
{:else}
Downloading Suyu
{/if}
</h1> -->
<p class="max-w-[36rem] text-lg leading-relaxed text-[#A6A5A7]">
Your download should start shortly. If it doesn't, click <a
href="https://gitlab.suyu.dev/suyu/suyu/releases">here</a
>.
</p>
</div>

View file

@ -0,0 +1,53 @@
<script lang="ts">
import tuta from "$assets/branding/tuta.png";
</script>
<svelte:head>
<title>FAQ</title>
</svelte:head>
<div
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-[#110d10] p-8 md:p-12"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="525"
viewBox="0 0 512 525"
fill="none"
style="animation-duration: 300s; transform-origin: 50% 50%; animation-iteration-count: infinite; animation-timing-function: linear; animation-name: spin; animation-delay: 0s; animation-direction: normal; animation-fill-mode: none; animation-play-state: running;"
class="pointer-events-none absolute -bottom-[18rem] right-0 z-0 animate-spin opacity-20"
>
<path
d="M511.5 262.12C511.5 353.613 465.547 434.182 396.019 480.947C408.179 457.937 415.083 431.597 415.083 403.617C415.083 313.723 343.816 240.744 255.992 240.744C191.257 240.744 138.692 186.941 138.692 120.622C138.692 54.3027 191.257 0.5 255.992 0.5C397.026 0.5 511.5 117.695 511.5 262.12ZM255.992 53.5225C243.745 53.5225 233.816 63.7047 233.816 76.2224C233.816 88.7388 243.745 98.9223 255.992 98.9223C268.257 98.9223 278.173 88.7387 278.173 76.2224C278.173 63.7048 268.257 53.5225 255.992 53.5225ZM299.355 97.9223C287.104 97.9223 277.173 108.104 277.173 120.622C277.173 133.139 287.104 143.322 299.355 143.322C311.62 143.322 321.536 133.139 321.536 120.622C321.536 108.104 311.62 97.9223 299.355 97.9223ZM212.635 97.9223C200.382 97.9223 190.455 108.104 190.455 120.622C190.455 133.139 200.382 143.322 212.635 143.322C224.889 143.322 234.816 133.139 234.816 120.622C234.816 108.104 224.888 97.9223 212.635 97.9223ZM255.992 142.322C243.745 142.322 233.816 152.505 233.816 165.021C233.816 177.539 243.745 187.721 255.992 187.721C268.257 187.721 278.173 177.538 278.173 165.021C278.173 152.505 268.257 142.322 255.992 142.322Z"
stroke="white"
/>
<path
d="M0.5 262.119C0.5 170.626 46.444 90.0553 115.976 43.2909C103.82 66.3019 96.9172 92.6424 96.9172 120.622C96.9172 210.516 168.174 283.495 255.992 283.495C320.735 283.495 373.305 337.298 373.305 403.617C373.305 469.934 320.735 523.739 255.992 523.739C114.974 523.739 0.5 406.544 0.5 262.119ZM255.992 336.517C243.744 336.517 233.816 346.7 233.816 359.217C233.816 371.735 243.745 381.917 255.992 381.917C268.256 381.917 278.173 371.735 278.173 359.217C278.173 346.701 268.256 336.517 255.992 336.517ZM299.355 380.917C287.104 380.917 277.173 391.099 277.173 403.617C277.173 416.135 287.104 426.317 299.355 426.317C311.619 426.317 321.536 416.135 321.536 403.617C321.536 391.099 311.619 380.917 299.355 380.917ZM255.992 425.317C243.745 425.317 233.816 435.499 233.816 448.016C233.816 460.533 243.744 470.717 255.992 470.717C268.256 470.717 278.173 460.533 278.173 448.016C278.173 435.499 268.256 425.317 255.992 425.317ZM212.634 380.917C200.382 380.917 190.454 391.099 190.454 403.617C190.454 416.135 200.382 426.317 212.634 426.317C224.888 426.317 234.816 416.135 234.816 403.617C234.816 391.099 224.888 380.917 212.634 380.917Z"
stroke="white"
/>
</svg>
<h1 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
FAQ
</h1>
<p class="max-w-[36rem] text-lg leading-relaxed text-[#A6A5A7]">
Got some questions? We got answers!
</p>
<p class="text-[15px] leading-[1.41] md:text-[19px] md:leading-[1.1]">Q: How is this project different from Yuzu? How do we know you won't have the same fate as Yuzu?</p>
<p class= "text-m text-[15px]">A: Unlike Yuzu, Suyu does <b>not</b> include many of the core "requirements" to run it. You need to legally dump your Nintendo Switch to obtain a title.keys file, which Yuzu did not do. Additionally, you must dump your own firmware.</p>
<p class="text-[15px] leading-[1.41] md:text-[19px] md:leading-[1.1]">Q: What is the purpose of Suyu?</p>
<p class= "text-m text-[15px]">A: The purpose of this project is to provide a free, open-source alternative to the now-dead Yuzu emulator. We believe that the community should be able to emulate their Switch device (legally) and be able to enjoy their favorite game titles.</p>
<p class="text-[15px] leading-[1.41] md:text-[19px] md:leading-[1.1]">Q: How can I contribute to Suyu?</p>
<p class= "text-m text-[15px]">A: You can contribute to this project by submitting a pull request on our <a href="https://git.suyu.dev/suyu/suyu">Git</a> page. We are always looking for new contributors to help us improve the project!</p>
<p class="text-[15px] leading-[1.41] md:text-[19px] md:leading-[1.1]">Q: Where can I download Suyu?</p>
<p class= "text-m text-[15px]">A: You can download the latest build of Suyu from our <a href="https://git.suyu.dev/suyu/suyu/releases">Git</a>. Please make sure you are using the right URL!</p>
<p class="text-[15px] leading-[1.41] md:text-[19px] md:leading-[1.1]">Q: What is the current progress for Suyu?</p>
<p class= "text-m text-[15px]">A: As of 3/20/2024, we have released our first Windows binary 🎉! You can find it <a href="https://git.suyu.dev/suyu/suyu/releases">here</a>. We are always trying to make more and more progress, so please feel free to <a href="https://discord.gg/suyu">join the Discord!</a></p>
<br />
<div class="leading-[1.41] items-center text-[15px] md:leading-[1.1] flex gap-2 text-gray-600">Email hosting lovingly provided by
<a href="https://tuta.com" target="_blank">
<img src={tuta} alt="Tuta" width={102} height={24} class="h-[24px] rounded-md" />
</a>
</div>
</div>

View file

@ -41,7 +41,6 @@ export async function POST({ request, getClientAddress }) {
if (!token) return new Response(null, { status: 401 });
// TODO: jwt utils which type and validate automatically
const user = await useAuth(token);
console.log(user);
if (!user) return new Response(null, { status: 401 });
const borkedIp = getClientAddress();
const room = RoomManager.createRoom({
@ -67,5 +66,11 @@ export async function POST({ request, getClientAddress }) {
hasPassword: body.hasPassword || false,
});
console.log("Room added:", JSON.stringify(room, null, 2));
// push every room to the top which starts with `[SUYU OFFICIAL]` and was created with username "suyu"
const suyuRoom = RoomManager.rooms.find((r) => r.roomInfo.name.startsWith("[SUYU OFFICIAL]"));
if (suyuRoom && suyuRoom.host.username === "suyu") {
RoomManager.rooms.splice(RoomManager.rooms.indexOf(suyuRoom), 1);
RoomManager.rooms.unshift(suyuRoom);
}
return json(room.toJSON());
}

View file

@ -5,6 +5,7 @@ import { useAuth } from "$lib/util/api/index.js";
/* thanks again janeberru for the shape of this data */
export async function POST({ request, params }) {
const body = await request.json();
console.log(body);
const { id } = params;
const room = RoomManager.getRoom(id);
if (!room) return new Response(null, { status: 500 });
@ -12,8 +13,9 @@ export async function POST({ request, params }) {
if (!user) return new Response(null, { status: 401 });
if (user.id !== room.host.id) return new Response(null, { status: 401 });
if (body.players.length === 0 && room.roomInfo.owner) {
console.log(room.roomInfo.players);
room.setPlayerList([{ gameId: 0, gameName: "", nickname: room.roomInfo.owner }]);
} else {
room.setPlayerList(body.players);
}
return json({ message: "Lobby updated successfully" });
}

View file

@ -0,0 +1,11 @@
import type { PageServerLoad } from "./$types";
import { getQueriedGamesAmerica, type GameUS } from "nintendo-switch-eshop";
export const load: PageServerLoad = async ({ params }) => {
const games = await getQueriedGamesAmerica(params.game);
return {
props: {
games,
},
};
};

View file

@ -0,0 +1,150 @@
<script lang="ts">
import ProgressBar from "$components/ProgressBar.svelte";
import { onMount } from "svelte";
import type { PageData } from "./$types";
import Logo from "$components/Logo.svelte";
import { page } from "$app/stores";
let shadersDone = 0;
const shadersTotal = 8146;
export let data: PageData;
$: game =
data.props.games.find(
(g) => g.title.trim().toLowerCase() === $page.params.game.trim().toLowerCase(),
) || data.props.games[0];
onMount(() => {
const interval = setInterval(() => {
shadersDone += Math.floor(Math.random() * 150);
if (shadersDone >= shadersTotal) {
clearInterval(interval);
shadersDone = shadersTotal;
}
}, 100);
});
</script>
<div class="body">
<div class="align-bottom">
<img
alt="Box art for {game.title}"
src={`https://assets.nintendo.com/image/upload/ar_16:9,c_lpad,w_656/b_white/f_auto/q_auto/${game.productImage}`}
/>
<div class="main-text">
<p class="launching">Launching <span class="bold">{game.title}</span></p>
<p>Shaders compiled: {shadersDone} / {shadersTotal}</p>
<div class="progress-bar">
<ProgressBar progress={shadersDone} total={shadersTotal} />
</div>
</div>
<div class="logo-spinner-container">
<div class="logo">
<Logo size={128} />
</div>
</div>
</div>
</div>
<style>
@keyframes spin {
/* 0% {
transform: none;
animation-timing-function: cubic-bezier(1, 0, 1, 1);
}
25% {
animation-timing-function: ease-out;
transform: scale(0.75) rotateZ(30deg);
}
30% {
transform: scale(0.75) rotateZ(10deg);
animation-timing-function: cubic-bezier(0.77, 0, 0.75, 0.37);
}
40% {
transform: scale(1.1) rotateZ(375deg);
animation-timing-function: cubic-bezier(0, 0.92, 0.21, 0.97);
}
42% {
transform: scale(1) rotateZ(780deg);
}
70%,
100% {
transform: scale(1) rotateZ(720deg);
} */
0% {
transform: none;
}
100% {
transform: rotateZ(360deg);
}
}
.logo-spinner-container {
margin-left: 120px;
}
.logo {
animation: spin 2s reverse infinite cubic-bezier(0.8, 0, 0.2, 1);
}
.body {
display: flex;
width: 100vw;
height: 100vh;
padding: 150px;
}
.progress-bar {
width: 100%;
margin-top: 16px;
}
.align-bottom {
display: flex;
justify-content: center;
align-items: flex-end;
height: 100%;
width: 100%;
}
.align-bottom img {
aspect-ratio: 1/1;
width: 300px;
object-fit: cover;
object-position: center center;
border: solid thin rgb(145, 173, 192);
border-radius: 24px;
box-shadow: 0 0 32px 0px rgba(145, 173, 192, 0.463);
}
.main-text {
width: calc(100% - 615px);
margin-left: 64px;
display: flex;
flex-direction: column;
gap: 8px;
flex-grow: 1;
}
.launching,
.launching > * {
font-size: 32px;
white-space: nowrap;
overflow: hidden;
}
.launching {
--mask-image: linear-gradient(
90deg,
black,
black calc(100% - 150px),
transparent calc(100% - 25px)
);
-webkit-mask-image: var(--mask-image);
mask-image: var(--mask-image);
}
.bold {
font-weight: bold;
width: 100%;
}
</style>

View file

@ -0,0 +1,228 @@
<script lang="ts">
import "$lib/css/fluent.css";
import Logo from "$components/Logo.svelte";
import close from "$assets/mockups/close.svg";
import maximize from "$assets/mockups/maximize.svg";
import minimize from "$assets/mockups/minimize.svg";
import Sidebar from "./components/Sidebar.svelte";
import { onMount, type SvelteComponent } from "svelte";
import LibraryPage from "./pages/Library.svelte";
import Library from "./components/icons/Library.svelte";
import Settings from "./components/icons/Settings.svelte";
import Community from "./components/icons/Community.svelte";
import Globe from "./components/icons/Globe.svelte";
import QA from "./components/icons/QA.svelte";
let Page: typeof SvelteComponent<{}>;
let tbMain: HTMLDivElement;
let tbFiller: HTMLDivElement;
let windowEl: HTMLDivElement;
let downPos: { x: number; y: number };
function changePage(e: CustomEvent<{ page: typeof SvelteComponent<{}> }>) {
Page = e.detail.page;
}
onMount(() => {
const left = Math.round((window.innerWidth - windowEl.offsetWidth) / 2);
const top = Math.round((window.innerHeight - windowEl.offsetHeight) / 2);
windowEl.style.left = `${left % 2 === 0 ? left : left + 1}px`;
windowEl.style.top = `${top % 2 === 0 ? top : top + 1}px`;
function onMove(e: MouseEvent) {
windowEl.style.left = `${windowEl.offsetLeft + e.clientX - downPos.x}px`;
windowEl.style.top = `${windowEl.offsetTop + e.clientY - downPos.y}px`;
downPos = { x: e.clientX, y: e.clientY };
}
function onMouseDown(e: MouseEvent) {
downPos = { x: e.clientX, y: e.clientY };
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onMouseUp);
}
function onMouseUp(e: MouseEvent) {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onMouseUp);
}
tbFiller.addEventListener("mousedown", onMouseDown);
tbMain.addEventListener("mousedown", onMouseDown);
return () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onMouseUp);
tbFiller.removeEventListener("mousedown", onMouseDown);
tbMain.removeEventListener("mousedown", onMouseDown);
};
});
</script>
<div class="root">
<div class="window mica-backdrop" bind:this={windowEl}>
<div class="window-contents">
<div class="titlebar-sidebar">
<div bind:this={tbMain} class="titlebar on-mica-bg">
<div class="titlebar-contents">
<div class="icon">
<Logo size={16} />
</div>
<div class="title">suyu | dev-1574a6818</div>
</div>
</div>
<div class="sidebar on-mica-bg">
<Sidebar
itemsTop={[
{
icon: Library,
text: "Library",
},
{
icon: Settings,
text: "Settings",
},
{
icon: Community,
text: "Multiplayer",
},
]}
itemsBottom={[
{
text: "Offical Website",
icon: Globe,
onclick: () => window.open("https://suyu.dev", "_blank"),
},
{
text: "Discord",
icon: QA,
onclick: () => window.open("https://discord.gg/suyu", "_blank"),
},
]}
on:changepage={changePage}
/>
</div>
</div>
<div class="filler-with-content">
<div bind:this={tbFiller} class="titlebar-filler on-mica-bg">
<div class="titlebar-buttons">
<div class="tb-button">
<img src={minimize} alt="Minimize" />
</div>
<div class="tb-button">
<img src={maximize} alt="Maximize" />
</div>
<div class="tb-button close">
<img src={close} alt="Close" />
</div>
</div>
</div>
<div class="window-body">
<svelte:component this={Page || LibraryPage} />
</div>
</div>
</div>
<div class="disclaimer">
<h1>Disclaimer</h1>
<p>
This is a <b>concept</b> for suyu's launcher, made by nullptr. It is not<br />a true
desktop application, it is non-functional and running in<br />a browser.
</p>
</div>
</div>
</div>
<style>
@keyframes window-appear {
from {
opacity: 0;
transform: scale(0.85);
}
to {
opacity: 1;
transform: scale(1);
}
}
.window-body {
flex-grow: 1;
}
.filler-with-content {
flex-grow: 1;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.titlebar-sidebar {
display: flex;
flex-direction: column;
height: 100%;
width: 250px;
}
.titlebar-filler {
height: 40px;
border-bottom: var(--fluent-stroke);
flex-shrink: 0;
}
.window-contents {
width: 100%;
height: 100%;
display: flex;
}
.sidebar {
height: 100%;
flex-grow: 1;
border-right: var(--fluent-stroke);
}
.titlebar-contents {
display: flex;
height: 40px;
padding: 10px 8px;
}
.title {
margin-top: -3px;
margin-left: 8px;
font-size: 14px;
}
.root {
width: 100vw;
height: 100vh;
background-image: url($assets/mockups/Screenshot.png);
background-position: bottom center;
}
.window {
width: 1012px;
height: 600px;
position: absolute;
}
.disclaimer {
text-align: right;
position: absolute;
bottom: 16px;
right: 24px;
}
.titlebar-buttons {
position: absolute;
right: 0;
top: 0;
display: flex;
}
.tb-button {
width: 46px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View file

@ -0,0 +1,91 @@
<script lang="ts">
import More from "./icons/More.svelte";
import smo from "$assets/mockups/smo.png";
</script>
<div class="card-container">
<div class="card-content">
<div class="card-image">
<img src={smo} alt="smo" />
</div>
<div class="card-body">
<div class="title">Super Mario Odyssey</div>
<div class="card-stats">1.1 KB • 382 hours</div>
</div>
</div>
<button class="card-more">
<More size={16} />
<span>More</span>
</button>
</div>
<style>
.title {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
.card-body {
width: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
font-size: 13px;
gap: 2px;
}
.card-stats {
opacity: 0.5;
font-size: 11px;
}
.card-image {
width: 100%;
height: 108px;
overflow: hidden;
flex-shrink: 0;
overflow: hidden;
border-radius: 4px;
border: var(--fluent-stroke);
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
filter: blur(0.5px);
}
.card-content {
flex-grow: 1;
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.card-container {
width: 128px;
height: 212px;
border-radius: 10px;
border: var(--fluent-stroke);
position: relative;
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.card-more {
width: 100%;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
</style>

View file

@ -0,0 +1,193 @@
<script lang="ts">
import { createEventDispatcher, onMount, type SvelteComponent } from "svelte";
interface SidebarItem {
icon: typeof SvelteComponent<{}>;
text: string;
onclick?: () => void;
}
let itemIndex = 0;
let pill: HTMLDivElement;
let sidebar: HTMLDivElement;
export let itemsTop: SidebarItem[];
export let itemsBottom: SidebarItem[];
const dispatch = createEventDispatcher<{
changepage: { page: typeof SvelteComponent<{}> };
}>();
function getIndex(item: SidebarItem) {
return Array.prototype.concat(itemsTop, itemsBottom).indexOf(item);
}
async function itemClick(item: SidebarItem, e: MouseEvent) {
if (item.onclick) return item.onclick();
const button = (e.target as HTMLElement).closest("button");
console.log(button);
if (!button) return;
const rect = button.getBoundingClientRect();
const sidebarRect = sidebar.getBoundingClientRect();
try {
const page = await import(`../pages/${item.text}.svelte`);
let prevItem = itemIndex;
itemIndex = getIndex(item);
if (prevItem === itemIndex) return;
const isDown = itemIndex > prevItem;
if (isDown) {
await pill.animate(
[
{
height: "28px",
},
],
{ duration: 150, easing: "ease-in" },
).finished;
pill.style.top = `${rect.top - sidebarRect.top}px`;
dispatch("changepage", { page: page.default });
await pill.animate(
[
{
height: "28px",
transform: "translateY(-4px)",
},
{
height: "16px",
},
],
{ duration: 150, easing: "ease-out", fill: "forwards" },
).finished;
} else {
await pill.animate(
[
{
height: "28px",
transform: "translateY(-2px)",
},
],
{ duration: 150, easing: "ease-in" },
).finished;
pill.style.top = `${rect.top - sidebarRect.top}px`;
dispatch("changepage", { page: page.default });
await pill.animate(
[
{
height: "28px",
},
{
height: "16px",
},
],
{ duration: 150, easing: "ease-out", fill: "forwards" },
).finished;
}
} catch {
console.error(`Page not found: ${item.text}`);
}
}
onMount(() => {
// i'm sorry orche
const firstItem = document.querySelector(".sidebar-item");
if (!firstItem) return;
const firstItemRect = firstItem.getBoundingClientRect();
const sidebarRect = sidebar.getBoundingClientRect();
pill.style.display = "block";
pill.style.top = `${firstItemRect.top - sidebarRect.top}px`;
pill.style.left = `${firstItemRect.left - sidebarRect.left + 1}px`;
});
</script>
<div class="sidebar" bind:this={sidebar}>
<div class="pill" bind:this={pill} />
<div class="sidebar-content top">
{#each itemsTop as item}
<button
on:click={(e) => {
itemClick(item, e);
}}
class="sidebar-item fluent-press"
>
<svelte:component this={item.icon} size={16} />
<p>{item.text}</p>
</button>
{/each}
</div>
<div class="sidebar-content bottom">
{#each itemsBottom as item}
<button
on:click={(e) => {
itemClick(item, e);
}}
class="sidebar-item fluent-press"
>
<svelte:component this={item.icon} size={16} />
<p>{item.text}</p>
</button>
{/each}
</div>
</div>
<style>
.pill {
width: 3px;
height: 16px;
background-color: #4e92dc;
border-radius: 8px;
display: none;
position: absolute;
transform: translateY(10px);
}
.sidebar {
display: flex;
flex-direction: column;
padding-bottom: 5px;
height: 100%;
position: relative;
}
.sidebar-content {
display: flex;
flex-direction: column;
padding: 0 4px;
gap: 4px;
}
.sidebar-content.top {
flex-grow: 1;
}
.sidebar-content.bottom {
flex-shrink: 0;
}
.sidebar-item {
appearance: none;
display: flex;
align-items: center;
height: 36px !important;
padding: 0 12px;
gap: 10px;
border-radius: 6px !important;
cursor: pointer;
border: none !important;
background-color: transparent !important;
}
.sidebar-item:hover {
background-color: rgba(200, 197, 197, 0.1) !important;
filter: none;
}
.sidebar-item:active {
background-color: rgba(154, 154, 154, 0.15) !important;
filter: none;
}
.sidebar-item > p {
font-size: 14px !important;
margin-top: -2px;
}
</style>

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let size: number = 24;
</script>
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
><path
d="M14.75 15c.966 0 1.75.784 1.75 1.75l-.001.962c.117 2.19-1.511 3.297-4.432 3.297-2.91 0-4.567-1.09-4.567-3.259v-1c0-.966.784-1.75 1.75-1.75h5.5Zm0 1.5h-5.5a.25.25 0 0 0-.25.25v1c0 1.176.887 1.759 3.067 1.759 2.168 0 2.995-.564 2.933-1.757V16.75a.25.25 0 0 0-.25-.25Zm-11-6.5h4.376a4.007 4.007 0 0 0-.095 1.5H3.75a.25.25 0 0 0-.25.25v1c0 1.176.887 1.759 3.067 1.759.462 0 .863-.026 1.207-.077a2.743 2.743 0 0 0-1.173 1.576l-.034.001C3.657 16.009 2 14.919 2 12.75v-1c0-.966.784-1.75 1.75-1.75Zm16.5 0c.966 0 1.75.784 1.75 1.75l-.001.962c.117 2.19-1.511 3.297-4.432 3.297l-.169-.002a2.755 2.755 0 0 0-1.218-1.606c.387.072.847.108 1.387.108 2.168 0 2.995-.564 2.933-1.757V11.75a.25.25 0 0 0-.25-.25h-4.28a4.05 4.05 0 0 0-.096-1.5h4.376ZM12 8a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 1.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3ZM6.5 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm11 0a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm-11 1.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm11 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
fill="#fff"
/></svg
>

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let size: number = 24;
</script>
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
><path
d="M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999ZM14.939 16.5H9.06c.652 2.414 1.786 4.002 2.939 4.002s2.287-1.588 2.939-4.002Zm-7.43 0H4.785a8.532 8.532 0 0 0 4.094 3.411c-.522-.82-.953-1.846-1.27-3.015l-.102-.395Zm11.705 0h-2.722c-.324 1.335-.792 2.5-1.373 3.411a8.528 8.528 0 0 0 3.91-3.127l.185-.283ZM7.094 10H3.735l-.005.017a8.525 8.525 0 0 0-.233 1.984c0 1.056.193 2.067.545 3h3.173a20.847 20.847 0 0 1-.123-5Zm8.303 0H8.603a18.966 18.966 0 0 0 .135 5h6.524a18.974 18.974 0 0 0 .135-5Zm4.868 0h-3.358c.062.647.095 1.317.095 2a20.3 20.3 0 0 1-.218 3h3.173a8.482 8.482 0 0 0 .544-3c0-.689-.082-1.36-.236-2ZM8.88 4.09l-.023.008A8.531 8.531 0 0 0 4.25 8.5h3.048c.314-1.752.86-3.278 1.583-4.41ZM12 3.499l-.116.005C10.62 3.62 9.396 5.622 8.83 8.5h6.342c-.566-2.87-1.783-4.869-3.045-4.995L12 3.5Zm3.12.59.107.175c.669 1.112 1.177 2.572 1.475 4.237h3.048a8.533 8.533 0 0 0-4.339-4.29l-.291-.121Z"
fill="#fff"
/></svg
>

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let size: number = 24;
</script>
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
><path
d="M4 3h1c1.054 0 1.918.816 1.995 1.85L7 5v14a2.001 2.001 0 0 1-1.85 1.994L5 21H4a2.001 2.001 0 0 1-1.995-1.85L2 19V5c0-1.054.816-1.918 1.85-1.995L4 3h1-1Zm6 0h1c1.054 0 1.918.816 1.995 1.85L13 5v14a2.001 2.001 0 0 1-1.85 1.994L11 21h-1a2.001 2.001 0 0 1-1.995-1.85L8 19V5c0-1.054.816-1.918 1.85-1.995L10 3h1-1Zm6.974 2c.84 0 1.608.531 1.89 1.346l.047.157 3.015 11.745a2 2 0 0 1-1.296 2.392l-.144.043-.969.248a2.002 2.002 0 0 1-2.387-1.284l-.047-.155-3.016-11.745a2 2 0 0 1 1.298-2.392l.143-.043.968-.248c.166-.043.334-.064.498-.064ZM5 4.5H4a.501.501 0 0 0-.492.41L3.5 5v14c0 .244.177.45.41.492L4 19.5h1c.245 0 .45-.178.492-.41L5.5 19V5a.501.501 0 0 0-.41-.492L5 4.5Zm6 0h-1a.501.501 0 0 0-.492.41L9.5 5v14c0 .244.177.45.41.492l.09.008h1c.245 0 .45-.178.492-.41L11.5 19V5a.501.501 0 0 0-.41-.492L11 4.5Zm5.975 2-.063.004-.063.013-.968.247a.498.498 0 0 0-.376.51l.015.1 3.016 11.745a.5.5 0 0 0 .483.375l.063-.003.062-.012.97-.25a.5.5 0 0 0 .374-.519l-.015-.088-3.015-11.747a.501.501 0 0 0-.483-.375Z"
fill="#fff"
/></svg
>

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let size: number = 24;
</script>
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
><path
d="M8 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM14 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM18 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"
fill="#ffffff"
/></svg
>

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let size: number = 24;
</script>
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
><path
d="M8.144 6.307c.434-.232.901-.306 1.356-.306.526 0 1.138.173 1.632.577.517.424.868 1.074.868 1.922 0 .975-.689 1.504-1.077 1.802l-.085.066c-.424.333-.588.511-.588.882a.75.75 0 0 1-1.5 0c0-1.134.711-1.708 1.162-2.062.513-.403.588-.493.588-.688 0-.397-.149-.622-.32-.761A1.115 1.115 0 0 0 9.5 7.5c-.295 0-.498.049-.65.13-.143.076-.294.21-.44.48a.75.75 0 1 1-1.32-.715c.264-.486.612-.853 1.054-1.089ZM9.5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fill="#fff"
/><path
d="M9.5 3a7.5 7.5 0 0 0-6.797 10.673l-.725 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.5 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.243-.121-.265.063-2.788.667c.2-.78.462-1.812.69-2.708l.07-.276-.13-.253A5.971 5.971 0 0 1 3.5 10.5Z"
fill="#fff"
/><path
d="M14.5 21c-1.97 0-3.761-.759-5.1-2h.1c.718 0 1.415-.089 2.081-.257.864.482 1.86.757 2.92.757.96 0 1.866-.225 2.669-.625l.243-.121.265.063c.921.22 1.965.445 2.74.61-.176-.751-.415-1.756-.642-2.651l-.07-.276.13-.253A5.971 5.971 0 0 0 20.5 13.5a5.995 5.995 0 0 0-2.747-5.042 8.443 8.443 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.51 2.1.672 2.803a1.244 1.244 0 0 1-1.468 1.5c-.727-.152-1.87-.396-2.913-.64A7.476 7.476 0 0 1 14.5 21Z"
fill="#fff"
/></svg
>

View file

@ -0,0 +1,10 @@
<script lang="ts">
export let size: number = 24;
</script>
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
><path
d="M12.012 2.25c.734.008 1.465.093 2.182.253a.75.75 0 0 1 .582.649l.17 1.527a1.384 1.384 0 0 0 1.927 1.116l1.401-.615a.75.75 0 0 1 .85.174 9.792 9.792 0 0 1 2.204 3.792.75.75 0 0 1-.271.825l-1.242.916a1.381 1.381 0 0 0 0 2.226l1.243.915a.75.75 0 0 1 .272.826 9.797 9.797 0 0 1-2.204 3.792.75.75 0 0 1-.848.175l-1.407-.617a1.38 1.38 0 0 0-1.926 1.114l-.169 1.526a.75.75 0 0 1-.572.647 9.518 9.518 0 0 1-4.406 0 .75.75 0 0 1-.572-.647l-.168-1.524a1.382 1.382 0 0 0-1.926-1.11l-1.406.616a.75.75 0 0 1-.849-.175 9.798 9.798 0 0 1-2.204-3.796.75.75 0 0 1 .272-.826l1.243-.916a1.38 1.38 0 0 0 0-2.226l-1.243-.914a.75.75 0 0 1-.271-.826 9.793 9.793 0 0 1 2.204-3.792.75.75 0 0 1 .85-.174l1.4.615a1.387 1.387 0 0 0 1.93-1.118l.17-1.526a.75.75 0 0 1 .583-.65c.717-.159 1.45-.243 2.201-.252Zm0 1.5a9.135 9.135 0 0 0-1.354.117l-.109.977A2.886 2.886 0 0 1 6.525 7.17l-.898-.394a8.293 8.293 0 0 0-1.348 2.317l.798.587a2.881 2.881 0 0 1 0 4.643l-.799.588c.32.842.776 1.626 1.348 2.322l.905-.397a2.882 2.882 0 0 1 4.017 2.318l.11.984c.889.15 1.798.15 2.687 0l.11-.984a2.881 2.881 0 0 1 4.018-2.322l.905.396a8.296 8.296 0 0 0 1.347-2.318l-.798-.588a2.881 2.881 0 0 1 0-4.643l.796-.587a8.293 8.293 0 0 0-1.348-2.317l-.896.393a2.884 2.884 0 0 1-4.023-2.324l-.11-.976a8.988 8.988 0 0 0-1.333-.117ZM12 8.25a3.75 3.75 0 1 1 0 7.5 3.75 3.75 0 0 1 0-7.5Zm0 1.5a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z"
fill="#fff"
/></svg
>

View file

@ -0,0 +1,16 @@
<script>
import Card from "../components/Card.svelte";
</script>
<div class="cards">
<Card />
</div>
<style>
.cards {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, 128px);
grid-gap: 20px;
}
</style>

View file

@ -0,0 +1 @@
<h1>Multiplayer</h1>

View file

@ -0,0 +1 @@
<h1>Settings</h1>