Merge 'github/dev' into dev
This commit is contained in:
commit
b068ab99f4
13 changed files with 858 additions and 176 deletions
445
+layout.svelte
Normal file
445
+layout.svelte
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import "../app.pcss";
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import Logo from "../components/LogoWithTextHorizontal.svelte";
|
||||||
|
import { CodeBranchOutline, DiscordSolid, BarsSolid, CloseSolid } from "flowbite-svelte-icons";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { setContext } from "svelte";
|
||||||
|
import type { TransitionConfig } from "svelte/transition";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
import { bounceOut } from "svelte/easing";
|
||||||
|
import { generateTransition, transition } from "$lib/util/animation";
|
||||||
|
import { reducedMotion } from "$lib/accessibility";
|
||||||
|
import BackgroundProvider from "$components/BackgroundProvider.svelte";
|
||||||
|
import AccountButton from "$components/AccountButton.svelte";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
// TODO: refactor
|
||||||
|
|
||||||
|
const navigator = browser ? window.navigator : { userAgent: "" };
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = writable(data.tokenCookie || "");
|
||||||
|
|
||||||
|
function transitionIn(node: HTMLElement, { duration = 360 }: TransitionConfig) {
|
||||||
|
if ($reducedMotion)
|
||||||
|
return {
|
||||||
|
duration: 0,
|
||||||
|
};
|
||||||
|
node = node.querySelector(".content") || node;
|
||||||
|
const intensity = node.dataset.intensityIn || "160";
|
||||||
|
const UA = navigator.userAgent;
|
||||||
|
const ff = UA.indexOf("Firefox") > -1;
|
||||||
|
if (!dropdownCloseFinished) {
|
||||||
|
node.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
top: `${intensity}px`,
|
||||||
|
opacity: "0",
|
||||||
|
filter: ff ? "none" : `blur(${parseInt(intensity) / 16}px)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
top: "0",
|
||||||
|
opacity: "1",
|
||||||
|
filter: ff ? "none" : "blur(0px)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
easing: transition,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// FUCK YOUR DEFAULT SYSTEM, SVELTEKIT!!!
|
||||||
|
node.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
top: `${-intensity}px`,
|
||||||
|
opacity: "0",
|
||||||
|
filter: ff ? "none" : `blur(${parseInt(intensity) / 16}px)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
top: "0",
|
||||||
|
opacity: "1",
|
||||||
|
filter: ff ? "none" : "blur(0px)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
easing: transition,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
duration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transitionOut(node: HTMLElement, { duration = 360 }: TransitionConfig) {
|
||||||
|
if ($reducedMotion)
|
||||||
|
return {
|
||||||
|
duration: 0,
|
||||||
|
};
|
||||||
|
node = node.querySelector(".content") || node;
|
||||||
|
const intensity = node.dataset.intensityOut || "240";
|
||||||
|
if (!dropdownCloseFinished)
|
||||||
|
return {
|
||||||
|
duration: 0,
|
||||||
|
};
|
||||||
|
const UA = navigator.userAgent;
|
||||||
|
const ff = UA.indexOf("Firefox") > -1;
|
||||||
|
node.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
top: "0",
|
||||||
|
opacity: "1",
|
||||||
|
filter: ff ? "none" : "blur(0px)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
top: `${intensity}px`,
|
||||||
|
opacity: "0",
|
||||||
|
filter: ff ? "none" : `blur(${parseInt(intensity) / 16}px)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
easing: transition,
|
||||||
|
duration: duration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
duration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let dropdownOpen = false;
|
||||||
|
let dropdownCloseFinished = true;
|
||||||
|
let dropdownOpenFinished = false;
|
||||||
|
// let dropdownOpen = true;
|
||||||
|
// let dropdownCloseFinished = false;
|
||||||
|
// let dropdownOpenFinished = true;
|
||||||
|
let scrolled = false;
|
||||||
|
let cookies: {
|
||||||
|
[key: string]: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
$: navItems = [
|
||||||
|
{
|
||||||
|
name: "Blog",
|
||||||
|
href: "/coming-soon",
|
||||||
|
title: "Coming Soon",
|
||||||
|
target: "_self",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Docs",
|
||||||
|
href: "/coming-soon",
|
||||||
|
title: "Coming Soon",
|
||||||
|
target: "_self",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FAQ",
|
||||||
|
href: "/faq",
|
||||||
|
title: "Frequently Asked Questions",
|
||||||
|
target: "_self",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Compatibility",
|
||||||
|
href: "https://docs.google.com/spreadsheets/u/0/d/1LrLak1DP4UP3bNZKOCAzwNEp5JMkVozYuMnUBDF8gQM/htmlview#",
|
||||||
|
title: "Compatibility",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Discord",
|
||||||
|
href: "https://discord.gg/suyu",
|
||||||
|
title: "Suyu Discord Server",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Git",
|
||||||
|
href: "https://git.suyu.dev/suyu/suyu",
|
||||||
|
title: "Suyu Git Repo",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: $token || data.tokenCookie ? "Account" : "Sign up",
|
||||||
|
// href: $token || data.tokenCookie ? "/account" : "/signup",
|
||||||
|
// },
|
||||||
|
$token || data.tokenCookie
|
||||||
|
? {
|
||||||
|
name: "Account",
|
||||||
|
href: "/account",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: "Sign up",
|
||||||
|
href: "/signup",
|
||||||
|
},
|
||||||
|
$token || data.tokenCookie
|
||||||
|
? {
|
||||||
|
name: "Log out",
|
||||||
|
href: "/logout",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: "Log in",
|
||||||
|
href: "/login",
|
||||||
|
},
|
||||||
|
] as NavItem[];
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (browser) {
|
||||||
|
const html = document.querySelector("html")!;
|
||||||
|
if (!dropdownOpen) {
|
||||||
|
dropdownCloseFinished = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
html.style.overflowY = "auto";
|
||||||
|
dropdownCloseFinished = true;
|
||||||
|
}, 360);
|
||||||
|
} else {
|
||||||
|
html.style.overflowY = "hidden";
|
||||||
|
dropdownOpenFinished = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
dropdownOpenFinished = true;
|
||||||
|
}, 360);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransition(i: number) {
|
||||||
|
return `${((i + 1) / 4) * 75}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
cookies = Object.fromEntries(
|
||||||
|
document.cookie.split("; ").map((c) => {
|
||||||
|
const [key, value] = c.split("=");
|
||||||
|
return [key, value];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (cookies.token) {
|
||||||
|
$token = cookies.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDropdown() {
|
||||||
|
dropdownOpen = !dropdownOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext("token", token);
|
||||||
|
onMount(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
scrolled = window.scrollY > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleScroll(); // we can't guarantee that the page starts at the top
|
||||||
|
|
||||||
|
cookies = Object.fromEntries(
|
||||||
|
document.cookie.split("; ").map((c) => {
|
||||||
|
const [key, value] = c.split("=");
|
||||||
|
return [key, value];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- unfortunately, firefox is horrendous at rendering transforms so we can't enable it there -->
|
||||||
|
{#if navigator.userAgent.indexOf("Firefox") === -1}
|
||||||
|
<div
|
||||||
|
class="opacity-5"
|
||||||
|
style="position: fixed; width: 100vw; height: 100vh; --mask-image: linear-gradient(to bottom, transparent 50px, black 150px, transparent); mask-image: var(--mask-image); -webkit-mask-image: var(--mask-image);"
|
||||||
|
>
|
||||||
|
<BackgroundProvider size={90} gap={16} speed={0.25} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="bg">
|
||||||
|
<div
|
||||||
|
style="background: radial-gradient(50% 50%, rgba(255,0,0,0.05), transparent); z-index: -1; width: 800px ;height: 800px; position: fixed; top: -50%; left: calc(25% - 400px);"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="background: radial-gradient(50% 50%, rgba(0,128,255,0.05), transparent); z-index: -1; width: 800px ;height: 800px; position: fixed; top: -50%; right: calc(25% - 400px);"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main
|
||||||
|
class={`min-h-full w-full ${dropdownOpen || !dropdownCloseFinished ? "overflow-hidden" : ""}`}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
style="transition: 180ms ease border;"
|
||||||
|
class={`${
|
||||||
|
scrolled
|
||||||
|
? "fixed top-0 z-[9999] w-full border-b-2 border-b-[#ffffff11] bg-[#131215d0]"
|
||||||
|
: "fixed top-0 z-[9999] w-full border-b-0 border-b-[transparent]"
|
||||||
|
} pl-[calc(100vw-100%)]`}
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
style="transition: 180ms ease;"
|
||||||
|
class={scrolled
|
||||||
|
? "mx-auto flex h-[72px] w-full max-w-[1300px] flex-row items-center justify-between px-8 backdrop-blur-xl"
|
||||||
|
: "mx-auto flex h-[120px] w-full max-w-[1300px] flex-row items-center justify-between px-8"}
|
||||||
|
>
|
||||||
|
<div class="flex w-full flex-row items-center justify-start gap-8">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
title="Suyu Home"
|
||||||
|
on:click={() => {
|
||||||
|
if (dropdownOpen && window.innerWidth < 800) toggleDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo size={28} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-row items-center justify-center gap-2 text-sm font-medium text-[#A6A5A7] max-[800px]:hidden"
|
||||||
|
>
|
||||||
|
<a href="/coming-soon" class="px-5 py-3 transition hover:text-white" title="Coming Soon">Blog</a>
|
||||||
|
<a href="/coming-soon" class="px-5 py-3 transition hover:text-white" title="Coming Soon">Docs</a>
|
||||||
|
<a href="/faq" class="px-5 py-3 transition hover:text-white" title="Frequently Asked Questions">FAQ</a>
|
||||||
|
<a href="https://docs.google.com/spreadsheets/u/0/d/1LrLak1DP4UP3bNZKOCAzwNEp5JMkVozYuMnUBDF8gQM/htmlview#" class="px-5 py-3 transition hover:text-white" title="Compatibility" target="_blank">Compatibility</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-row items-center justify-end text-[#A6A5A7]">
|
||||||
|
<div class="flex flex-row gap-4 max-[800px]:hidden">
|
||||||
|
<a
|
||||||
|
class="p-2 transition hover:text-white"
|
||||||
|
href="https://git.suyu.dev/suyu/suyu"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
title="Suyu Git Repo"
|
||||||
|
>
|
||||||
|
<CodeBranchOutline />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="p-2 transition hover:text-white"
|
||||||
|
href="https://discord.gg/suyu"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
title="Suyu Discord Server"
|
||||||
|
>
|
||||||
|
<DiscordSolid />
|
||||||
|
</a>
|
||||||
|
{#if $token}
|
||||||
|
<!-- <a href={$token ? "/account" : "/signup"} class="button-sm"
|
||||||
|
>{$token ? "Account" : "Sign up"}</a
|
||||||
|
> -->
|
||||||
|
<!-- <a href="/account" class="button-sm">Account</a> -->
|
||||||
|
<AccountButton user={data.user} />
|
||||||
|
{:else}
|
||||||
|
<a href="/login" class="button-sm">Log in</a>
|
||||||
|
<a href="/signup" class="button-sm">Sign up</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="relative mr-4 hidden flex-row gap-4 max-[800px]:flex">
|
||||||
|
<button
|
||||||
|
aria-label={dropdownOpen ? "Close navigation" : "Open navigation"}
|
||||||
|
aria-expanded={dropdownOpen}
|
||||||
|
on:click={toggleDropdown}
|
||||||
|
class="-mr-4 p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="transition: 180ms; transition-property: opacity transform;"
|
||||||
|
class={`absolute ${dropdownOpen ? "rotate-45 opacity-0" : "opacity-1"}`}
|
||||||
|
>
|
||||||
|
<BarsSolid />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="transition: 180ms; transition-property: opacity transform;"
|
||||||
|
class={dropdownOpen
|
||||||
|
? "opacity-1 rotate-0"
|
||||||
|
: "rotate-[-45deg] opacity-0"}
|
||||||
|
>
|
||||||
|
<CloseSolid />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
style="transition: 180ms ease;"
|
||||||
|
aria-hidden={!dropdownOpenFinished && !dropdownOpen}
|
||||||
|
class={`fixed left-0 z-[100] h-screen w-full bg-[#0e0d10] p-9 pt-[120px] ${dropdownOpen ? "pointer-events-auto visible opacity-100" : "pointer-events-none opacity-0"} ${!dropdownOpen && dropdownCloseFinished ? "invisible" : ""}`}
|
||||||
|
>
|
||||||
|
<div class={`flex flex-col gap-8`}>
|
||||||
|
<!-- <a href="##"><h1 class="w-full text-5xl">Blog</h1></a>
|
||||||
|
<a href="##"><h1 class="w-full text-5xl">Docs</h1></a>
|
||||||
|
<a href="##"><h1 class="w-full text-5xl">FAQ</h1></a> -->
|
||||||
|
{#each navItems as item, i}
|
||||||
|
<a
|
||||||
|
style={`transition: ${
|
||||||
|
dropdownOpen
|
||||||
|
? generateTransition([
|
||||||
|
// {
|
||||||
|
// duration: 600,
|
||||||
|
// delay: (i + 1) / 4,
|
||||||
|
// property: "transform",
|
||||||
|
// timingFunction: transition,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// duration: 500,
|
||||||
|
// delay: i * 1.25,
|
||||||
|
// property: "filter",
|
||||||
|
// timingFunction: transition,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// duration: 400,
|
||||||
|
// delay: (i + 1) / 4,
|
||||||
|
// property: "opacity",
|
||||||
|
// timingFunction: transition,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
duration: 450,
|
||||||
|
delay: i * 0.6,
|
||||||
|
property: ["transform", "opacity", "filter"],
|
||||||
|
timingFunction: transition,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
: generateTransition([
|
||||||
|
{
|
||||||
|
duration: 450,
|
||||||
|
delay: 0,
|
||||||
|
property: ["transform", "opacity", "filter"],
|
||||||
|
timingFunction: transition,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}`}
|
||||||
|
class="{dropdownOpen
|
||||||
|
? 'translate-y-0 opacity-100 filter-none'
|
||||||
|
: '-translate-y-24 opacity-0 blur-md'} "
|
||||||
|
href={item.href}
|
||||||
|
title={item.title}
|
||||||
|
target={item.target}
|
||||||
|
on:click={() => toggleDropdown()}
|
||||||
|
>
|
||||||
|
<h1 class="w-full text-5xl">{item.name}</h1>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
{#key data.url}
|
||||||
|
<div
|
||||||
|
in:transitionIn={{ duration: 500 }}
|
||||||
|
out:transitionOut={{ duration: 500 }}
|
||||||
|
aria-hidden={dropdownOpenFinished && dropdownOpen}
|
||||||
|
tabindex={dropdownOpen ? 0 : -1}
|
||||||
|
class={`absolute left-[50%] z-50 mx-auto flex w-screen max-w-[1300px] translate-x-[-50%] flex-col px-8 pb-12 pt-[120px] ${dropdownOpen ? "pointer-events-none translate-y-[25vh] opacity-0" : ""} ${dropdownOpenFinished && dropdownOpen ? "invisible" : ""}`}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
</main>
|
279
+page.svelte
Normal file
279
+page.svelte
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
<script lang="ts">
|
||||||
|
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";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
$: memberCount = parseFloat(data.memberCount?.toPrecision(2));
|
||||||
|
$: contributors = parseFloat(data.roleMembers["1214817156420862012"]?.toPrecision(2));
|
||||||
|
$: starCount = parseFloat(data.starCount?.toPrecision(2));
|
||||||
|
let metadata = {
|
||||||
|
url: "https://suyu.dev",
|
||||||
|
title: "suyu - Open-source, non-profit Switch emulator",
|
||||||
|
description:
|
||||||
|
"suyu is a familiar C++ based Nintendo Switch emulator with a focus on compatibility. Completely free and open-source, forever. Download it here.",
|
||||||
|
image: embedImage,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rootOpen: boolean;
|
||||||
|
let rootModal: boolean = true;
|
||||||
|
let contentOpenAutoFocus: boolean = true;
|
||||||
|
let contentCloseAutoFocus: boolean = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
<meta name="description" content={metadata.description} />
|
||||||
|
|
||||||
|
<meta property="og:url" content={metadata.url} />
|
||||||
|
<meta property="og:title" content={metadata.title} />
|
||||||
|
<meta property="og:description" content={metadata.description} />
|
||||||
|
<meta property="og:image" content={metadata.image} />
|
||||||
|
|
||||||
|
<meta name="twitter:url" content={metadata.url} />
|
||||||
|
<meta name="twitter:title" content={metadata.title} />
|
||||||
|
<meta name="twitter:description" content={metadata.description} />
|
||||||
|
<meta name="twitter:image" content={metadata.image} />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] rounded-bl-none rounded-br-none bg-[#110d10] p-8 md:p-12 lg:rounded-bl-none lg:rounded-br-[2.25rem]"
|
||||||
|
>
|
||||||
|
<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-[56px] md:leading-[1.1]">
|
||||||
|
<span class="font-bold text-[#60c7e9]">suyu</span> is a fully open-source
|
||||||
|
<span class="font-bold text-[#f94d4d]">Switch</span> emulator
|
||||||
|
</h1>
|
||||||
|
<p class="max-w-[36rem] text-lg leading-relaxed text-[#A6A5A7]">
|
||||||
|
suyu is a familiar C++ based Switch emulator with a focus on compatibility. Completely free
|
||||||
|
and open-source, forever.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-4 md:flex-row">
|
||||||
|
<a
|
||||||
|
href="/download"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="cta-button"
|
||||||
|
title="Download Suyu"
|
||||||
|
>
|
||||||
|
Download <svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
role="img"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
><path
|
||||||
|
d="M5.46967 11.4697C5.17678 11.7626 5.17678 12.2374 5.46967 12.5303C5.76256 12.8232 6.23744 12.8232 6.53033 12.5303L10.5303 8.53033C10.8207 8.23999 10.8236 7.77014 10.5368 7.47624L6.63419 3.47624C6.34492 3.17976 5.87009 3.17391 5.57361 3.46318C5.27713 3.75244 5.27128 4.22728 5.56054 4.52376L8.94583 7.99351L5.46967 11.4697Z"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://git.suyu.dev/suyu/suyu"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="button text-[#8A8F98]"
|
||||||
|
title="Suyu Git Repo"
|
||||||
|
>
|
||||||
|
Contribute <svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
role="img"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
><path
|
||||||
|
d="M5.46967 11.4697C5.17678 11.7626 5.17678 12.2374 5.46967 12.5303C5.76256 12.8232 6.23744 12.8232 6.53033 12.5303L10.5303 8.53033C10.8207 8.23999 10.8236 7.77014 10.5368 7.47624L6.63419 3.47624C6.34492 3.17976 5.87009 3.17391 5.57361 3.46318C5.27713 3.75244 5.27128 4.22728 5.56054 4.52376L8.94583 7.99351L5.46967 11.4697Z"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col lg:flex-row">
|
||||||
|
<div
|
||||||
|
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>
|
||||||
|
<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
|
||||||
|
class="flex w-full flex-1 rounded-[2.25rem] rounded-bl-none rounded-tr-none bg-[#131215] pt-8 lg:pl-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full w-full overflow-hidden rounded-[2.25rem] bg-[#110d10] shadow-lg ring ring-[#ffffff11]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="h-full max-h-[496px] w-full object-cover"
|
||||||
|
alt=""
|
||||||
|
decoding="async"
|
||||||
|
loading="lazy"
|
||||||
|
src={suyuWindow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative mt-48 flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-[#eee] p-8 text-black md:p-12"
|
||||||
|
>
|
||||||
|
<h1 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
|
||||||
|
Built by and for the community
|
||||||
|
</h1>
|
||||||
|
<p class="max-w-[40rem] text-lg leading-relaxed text-[#4D4D4D]">
|
||||||
|
The future of suyu is shaped by you. New features are always being added, and our community
|
||||||
|
continually shapes the direction of the project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex w-full flex-col gap-8 lg:flex-row">
|
||||||
|
<a
|
||||||
|
href="https://discord.gg/suyu/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="relative w-full rounded-[2.25rem] bg-[#5865F2] p-12"
|
||||||
|
title="Suyu Discord Server"
|
||||||
|
>
|
||||||
|
<h2 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">Discord</h2>
|
||||||
|
<p class="mt-2 text-lg leading-relaxed">
|
||||||
|
Join our Discord server to chat with {memberCount}+ suyu users and developers. Get the
|
||||||
|
latest updates and help with any issues you have.
|
||||||
|
</p>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="absolute right-12 top-12 h-12 w-12"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="4"
|
||||||
|
d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
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"
|
||||||
|
title="Suyu Git Repo"
|
||||||
|
>
|
||||||
|
<h2 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">Git</h2>
|
||||||
|
<p class="mt-2 text-lg leading-relaxed">
|
||||||
|
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
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="absolute right-12 top-12 h-12 w-12"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="4"
|
||||||
|
d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative mt-48 flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-[#110d10] p-8 md:p-12"
|
||||||
|
>
|
||||||
|
<h1 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
|
||||||
|
we’re passionate about preserving games.
|
||||||
|
</h1>
|
||||||
|
<p class="max-w-[40rem] text-lg leading-relaxed text-[#A6A5A7]">
|
||||||
|
We’re developing suyu as a tool to prevent Switch games from becoming lost media, so we’re
|
||||||
|
taking care to prevent dissolution from outside influences.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
<p>suyu is community-run.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
<p>suyu is completely free. There’s no option to monetarily support the project.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
<p>suyu exists solely as an effort of hardware preservation and research.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
<p>suyu does not condone nor facilitate piracy or intellectual property theft.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-spacer-element class="min-h-[180px]"></div>
|
|
@ -1,6 +1,6 @@
|
||||||
import type { IJwtData } from "$types/auth";
|
import type { IJwtData } from "$types/auth";
|
||||||
import type { Role } from "$types/db";
|
import type { Role } from "$types/db";
|
||||||
import { PUBLIC_KEY } from "../secrets/secrets.json";
|
import { PUB_KEY } from "$env/static/private";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
export function json<T>(body: T, status?: number): Response {
|
export function json<T>(body: T, status?: number): Response {
|
||||||
|
@ -14,7 +14,7 @@ export function json<T>(body: T, status?: number): Response {
|
||||||
|
|
||||||
export async function getJwtData(token: string): Promise<IJwtData> {
|
export async function getJwtData(token: string): Promise<IJwtData> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] }, (err, data) => {
|
jwt.verify(token, PUB_KEY, { algorithms: ["RS256"] }, (err, data) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve(data as IJwtData);
|
else resolve(data as IJwtData);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { userRepo } from "$lib/server/repo";
|
import { userRepo } from "$lib/server/repo";
|
||||||
import type { SuyuUser } from "$lib/server/schema";
|
import type { SuyuUser } from "$lib/server/schema";
|
||||||
import { PUBLIC_KEY } from "$lib/server/secrets/secrets.json";
|
import { PUB_KEY } from "$env/static/private";
|
||||||
import type { IJwtData } from "$types/auth";
|
import type { IJwtData } from "$types/auth";
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
@ -18,7 +18,7 @@ export async function useAuth(
|
||||||
}
|
}
|
||||||
if (apiKey.startsWith("Bearer ")) {
|
if (apiKey.startsWith("Bearer ")) {
|
||||||
const token = apiKey.replace("Bearer ", "");
|
const token = apiKey.replace("Bearer ", "");
|
||||||
const decoded: IJwtData = jwt.verify(token, Buffer.from(PUBLIC_KEY), {
|
const decoded: IJwtData = jwt.verify(token, Buffer.from(PUB_KEY), {
|
||||||
algorithms: ["RS256"],
|
algorithms: ["RS256"],
|
||||||
}) as IJwtData;
|
}) as IJwtData;
|
||||||
let user = await userRepo.findOne({
|
let user = await userRepo.findOne({
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { building } from "$app/environment";
|
import { building } from "$app/environment";
|
||||||
import { DISCORD_USER_TOKEN, GITLAB_API_TOKEN } from "$env/static/private";
|
import { DISCORD_USER_TOKEN } from "$env/static/private";
|
||||||
|
|
||||||
let memberCount = 0;
|
let memberCount = 0;
|
||||||
let starCount = 0;
|
|
||||||
let roleMembers = {
|
let roleMembers = {
|
||||||
"1214817156420862012": 50,
|
"1214817156420862012": 50,
|
||||||
};
|
};
|
||||||
|
let gitCommits = 0;
|
||||||
|
|
||||||
async function fetchServerSideData() {
|
async function fetchServerSideData() {
|
||||||
console.log("Fetching member count");
|
console.log("Fetching member count");
|
||||||
|
@ -19,25 +19,20 @@ async function fetchServerSideData() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: Promise.resolve({ json: () => roleMembers }),
|
: Promise.resolve({ json: () => roleMembers }),
|
||||||
GITLAB_API_TOKEN
|
fetch('https://git.suyu.dev/api/v1/repos/suyu/suyu/commits?stat=false&verification=false&files=false&limit=1')
|
||||||
? fetch("https://gitlab.com/api/v4/projects/suyu-emu%2Fsuyu/", {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${GITLAB_API_TOKEN}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: Promise.resolve({ json: () => ({ star_count: 0 }) }), // Default to 0 if no token is provided
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [res, roles, gitlabRes] = await Promise.all(promises);
|
const [res, roles, git] = await Promise.all(promises);
|
||||||
const jsonPromises = [res.json(), roles.json(), gitlabRes.json()];
|
const jsonPromises = [res.json(), roles.json()];
|
||||||
const [resJson, rolesJson, gitlabResJson] = await Promise.all(jsonPromises);
|
const [resJson, rolesJson] = await Promise.all(jsonPromises);
|
||||||
|
|
||||||
|
|
||||||
memberCount = resJson.approximate_member_count;
|
memberCount = resJson.approximate_member_count;
|
||||||
starCount = gitlabResJson.star_count;
|
gitCommits = parseInt(git?.headers?.get('x-total'), 10) || 0;
|
||||||
if (DISCORD_USER_TOKEN) roleMembers = rolesJson;
|
if (DISCORD_USER_TOKEN) roleMembers = rolesJson;
|
||||||
|
|
||||||
console.log("Member count:", memberCount);
|
console.log("Member count:", memberCount);
|
||||||
console.log("Stars count:", starCount);
|
console.log('Git commit count:', gitCommits);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!building) {
|
if (!building) {
|
||||||
|
@ -50,7 +45,7 @@ export async function load({ cookies }) {
|
||||||
return {
|
return {
|
||||||
tokenCookie: token,
|
tokenCookie: token,
|
||||||
memberCount,
|
memberCount,
|
||||||
starCount,
|
|
||||||
roleMembers,
|
roleMembers,
|
||||||
|
gitCommits
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
$: memberCount = parseFloat(data.memberCount?.toPrecision(2));
|
$: memberCount = parseFloat(data.memberCount?.toPrecision(2));
|
||||||
$: contributors = parseFloat(data.roleMembers["1214817156420862012"]?.toPrecision(2));
|
$: contributors = parseFloat(data.roleMembers?.["1214817156420862012"]?.toPrecision(2));
|
||||||
$: starCount = parseFloat(data.starCount?.toPrecision(2));
|
$: gitCommits = parseFloat(data.gitCommits?.toPrecision(2));
|
||||||
let metadata = {
|
let metadata = {
|
||||||
url: "https://suyu.dev",
|
url: "https://suyu.dev",
|
||||||
title: "suyu - Open-source, non-profit Switch emulator",
|
title: "suyu - Open-source, non-profit Switch emulator",
|
||||||
|
@ -70,12 +70,8 @@
|
||||||
and open-source, forever.
|
and open-source, forever.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col gap-4 md:flex-row">
|
<div class="flex flex-col gap-4 md:flex-row">
|
||||||
<a
|
<a href="/download" rel="noreferrer noopener" class="cta-button">
|
||||||
href="/download"
|
Download <svg
|
||||||
rel="noreferrer noopener"
|
|
||||||
class="cta-button"
|
|
||||||
>
|
|
||||||
Download <svg
|
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
|
@ -116,7 +112,7 @@
|
||||||
>
|
>
|
||||||
<h1 class="text-[48px] leading-[0.9]">By the numbers</h1>
|
<h1 class="text-[48px] leading-[0.9]">By the numbers</h1>
|
||||||
<HomepageCounter count={contributors} subText="dedicated contributors" />
|
<HomepageCounter count={contributors} subText="dedicated contributors" />
|
||||||
<HomepageCounter count={starCount} subText="GitLab stars" />
|
<HomepageCounter count={gitCommits} subText="Git commits" />
|
||||||
<HomepageCounter count={4000} subText="supported games" />
|
<HomepageCounter count={4000} subText="supported games" />
|
||||||
<HomepageCounter count={memberCount} subText="members on Discord" />
|
<HomepageCounter count={memberCount} subText="members on Discord" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -186,8 +182,8 @@
|
||||||
>
|
>
|
||||||
<h2 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">Git</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">
|
<p class="mt-2 text-lg leading-relaxed">
|
||||||
Our Git instance 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
|
||||||
to help us out, so feel free to check out our code.
|
contributors to help us out, so feel free to check out our code.
|
||||||
</p>
|
</p>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
// },
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
function navClick(e: MouseEvent | HTMLAnchorElement) {
|
function navClick(e: MouseEvent | HTMLAnchorElement, ignoreAnimation = false) {
|
||||||
const navBars = document.querySelectorAll<HTMLDivElement>(".navbar");
|
const navBars = document.querySelectorAll<HTMLDivElement>(".navbar");
|
||||||
if (navBars.length !== 1)
|
if (navBars.length !== 1)
|
||||||
navBars.forEach((bar) => {
|
navBars.forEach((bar) => {
|
||||||
|
@ -52,6 +52,7 @@
|
||||||
const bounds = target.getBoundingClientRect();
|
const bounds = target.getBoundingClientRect();
|
||||||
const navBounds = navBar.getBoundingClientRect();
|
const navBounds = navBar.getBoundingClientRect();
|
||||||
const pillBounds = indicator.getBoundingClientRect();
|
const pillBounds = indicator.getBoundingClientRect();
|
||||||
|
if (ignoreAnimation) return;
|
||||||
indicator.style.transform = `translateX(${bounds.left - navBounds.left}px)`;
|
indicator.style.transform = `translateX(${bounds.left - navBounds.left}px)`;
|
||||||
indicator.style.width = `${bounds.width}px`;
|
indicator.style.width = `${bounds.width}px`;
|
||||||
if (
|
if (
|
||||||
|
@ -190,7 +191,7 @@
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
data-index={i}
|
data-index={i}
|
||||||
on:click={navClick}
|
on:click={(e) => navClick(e, true)}
|
||||||
class={`navitem flex flex-grow basis-[0] items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-bold ${
|
class={`navitem flex flex-grow basis-[0] items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-bold ${
|
||||||
selected === i ? " text-[#a9a9a9] opacity-100" : "opacity-50"
|
selected === i ? " text-[#a9a9a9] opacity-100" : "opacity-50"
|
||||||
}`}
|
}`}
|
||||||
|
|
|
@ -1,74 +1,80 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
// Allow for us to use client-sided code when needed
|
||||||
onMount(async () => {
|
import { onMount } from "svelte";
|
||||||
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
|
// cool moving dots :3
|
||||||
if (latestRelease) {
|
// NOTE: This is required to be ran on the server due to issues with Svelte
|
||||||
console.log("Latest release tag:", latestRelease.name);
|
let text = "Downloading Suyu";
|
||||||
return latestRelease.name; // Assuming the first result is the latest
|
const textLength = text.length
|
||||||
} else {
|
setInterval(() => {
|
||||||
console.log("No releases found.");
|
text += ".";
|
||||||
return null;
|
// Text length + 3 is essentially the length of the text above + the 3 dots
|
||||||
}
|
if (text.length > textLength + 3) {
|
||||||
} catch (error) {
|
text = "Downloading Suyu";
|
||||||
console.error("Error fetching latest release tag:", error);
|
}
|
||||||
return null;
|
}, 500);
|
||||||
}
|
|
||||||
}
|
$: htmlContent = `${text}`;
|
||||||
const latestRelease = await getTag();
|
|
||||||
setTimeout(() => {
|
onMount(async () => {
|
||||||
if (UA.includes("Windows")) {
|
// Variables
|
||||||
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/raw/master/${latestRelease}/Suyu-Windows_x64.7z`;
|
const UA = navigator.userAgent;
|
||||||
} else if (UA.includes("Linux")) {
|
const url = `https://git.suyu.dev/api/v1/repos/suyu/suyu/tags`;
|
||||||
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/raw/master/${latestRelease}/suyu-mainline--.AppImage`;
|
const fakeVersionTag = false; // Fake version tag? (for debugging)
|
||||||
} else if (UA.includes("Macintosh;")) {
|
let latestRelease = "";
|
||||||
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/raw/master/${latestRelease}/suyu-macOS-arm64.dmg?inline=false`;
|
|
||||||
} else {
|
async function getTag() {
|
||||||
window.location.href = `https://gitlab.com/suyu-emu/suyu-releases/-/blob/master/${latestRelease}/`;
|
try {
|
||||||
}
|
// Get the latest release tag
|
||||||
}, 3000);
|
const response = await fetch(url, {
|
||||||
})
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort the response & get the first release (usually the latest)
|
||||||
|
/* TODO: Make the process way cleaner */
|
||||||
|
const releases = await response.json();
|
||||||
|
const latestRelease = releases[0].name;
|
||||||
|
|
||||||
|
console.log(latestRelease);
|
||||||
|
// Release found
|
||||||
|
if (latestRelease) {
|
||||||
|
console.log("Latest release tag:", latestRelease);
|
||||||
|
return latestRelease; // 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fakeVersionTag) {
|
||||||
|
latestRelease = await getTag();
|
||||||
|
} else {
|
||||||
|
latestRelease = "v0.0.1";
|
||||||
|
console.log(latestRelease);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (UA.includes("Windows")) {
|
||||||
|
window.location.href = `https://git.suyu.dev/suyu/suyu/releases/download/${latestRelease}/Suyu-Windows_x86_64.7z`;
|
||||||
|
// Android is above Linux because Android UA's also contain "Linux"
|
||||||
|
} else if (UA.includes("Android")) {
|
||||||
|
window.location.href = `https://git.suyu.dev/suyu/suyu/releases/download/${latestRelease}/Suyu-Android_Arm64.apk`;
|
||||||
|
} else if (UA.includes("Linux")) {
|
||||||
|
window.location.href = `https://git.suyu.dev/suyu/suyu/releases/download/${latestRelease}/Suyu-Linux_x86_64.AppImage`;
|
||||||
|
} else if (UA.includes("Macintosh;")) {
|
||||||
|
window.location.href = `https://git.suyu.dev/suyu/suyu/releases/download/${latestRelease}/Suyu-macOS-Arm64.dmg`;
|
||||||
|
} else {
|
||||||
|
window.location.href = `https://git.suyu.dev/suyu/suyu/releases/${latestRelease}/`;
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Downloading Suyu</title>
|
<title>Downloading Suyu</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
@ -94,36 +100,14 @@
|
||||||
stroke="white"
|
stroke="white"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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> -->
|
|
||||||
|
|
||||||
|
|
||||||
|
<h1 class="text-[24px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
|
||||||
|
{@html htmlContent}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<p class="max-w-[36rem] text-lg leading-relaxed text-[#A6A5A7]">
|
<p class="max-w-[36rem] text-lg leading-relaxed text-[#A6A5A7]">
|
||||||
Your download should start shortly. If it doesn't, click <a
|
Your download should start shortly. If it doesn't, click <a
|
||||||
href="https://gitlab.suyu.dev/suyu/suyu/releases">here</a
|
href="https://git.suyu.dev/suyu/suyu/releases"><u>here</u></a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,11 +39,11 @@
|
||||||
<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-[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-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-[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-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"><u>Git</u></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-[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-m text-[15px]">A: You can download the latest build of Suyu from our <a href="https://git.suyu.dev/suyu/suyu/releases"><u>Git</u></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-[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>
|
<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"><u>here</u></a>. We are always trying to make more and more progress, so please feel free to <a href="https://discord.gg/suyu"><u>join the Discord!</u></a></p>
|
||||||
<br />
|
<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
|
<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">
|
<a href="https://tuta.com" target="_blank">
|
||||||
|
|
24
src/routes/jwt/external/[audience]/+server.ts
vendored
Normal file
24
src/routes/jwt/external/[audience]/+server.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { PRIVATE_KEY } from "$env/static/private";
|
||||||
|
import { userRepo } from "$lib/server/repo/index.js";
|
||||||
|
import { getJwtData } from "$lib/server/util/index.js";
|
||||||
|
import { useAuth } from "$lib/util/api/index.js";
|
||||||
|
import type { IJwtData } from "$types/auth.js";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const jwtData = await getJwtData(request.headers.get("authorization")?.split(" ")[1] || "");
|
||||||
|
const user = await userRepo.findOne({
|
||||||
|
where: {
|
||||||
|
apiKey: jwtData.apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const token = jwt.sign({ ...user }, Buffer.from(PRIVATE_KEY), {
|
||||||
|
algorithm: "RS256",
|
||||||
|
});
|
||||||
|
return new Response(token, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/html",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
44
src/routes/jwt/external/key.pem/+server.ts
vendored
44
src/routes/jwt/external/key.pem/+server.ts
vendored
|
@ -1,45 +1,11 @@
|
||||||
import { json } from "$lib/server/util/index.js";
|
import { PUB_KEY } from "$env/static/private";
|
||||||
|
|
||||||
export function GET({ request }) {
|
export function GET({ request }) {
|
||||||
return new Response(
|
return new Response(PUB_KEY, {
|
||||||
`-----BEGIN CERTIFICATE-----
|
headers: {
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
"content-type": "text/plain",
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
|
||||||
-----END CERTIFICATE-----`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"content-type": "text/plain",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function POST({ request }) {
|
export function POST({ request }) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PRIVATE_KEY } from "$lib/server/secrets/secrets.json";
|
import { PRIVATE_KEY } from "$env/static/private";
|
||||||
import { useAuth } from "$lib/util/api/index.js";
|
import { useAuth } from "$lib/util/api/index.js";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Room, RoomManager } from "$lib/server/class/Room";
|
import { Room, RoomManager } from "$lib/server/class/Room";
|
||||||
import { userRepo } from "$lib/server/repo/index.js";
|
import { userRepo } from "$lib/server/repo/index.js";
|
||||||
import { SuyuUser } from "$lib/server/schema";
|
import { SuyuUser } from "$lib/server/schema";
|
||||||
import { PUBLIC_KEY } from "$lib/server/secrets/secrets.json";
|
|
||||||
import { json } from "$lib/server/util";
|
import { json } from "$lib/server/util";
|
||||||
import { useAuth } from "$lib/util/api/index.js";
|
import { useAuth } from "$lib/util/api/index.js";
|
||||||
import type { IJwtData } from "$types/auth.js";
|
import type { IJwtData } from "$types/auth.js";
|
||||||
|
@ -65,12 +64,5 @@ export async function POST({ request, getClientAddress }) {
|
||||||
host: user,
|
host: user,
|
||||||
hasPassword: body.hasPassword || false,
|
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());
|
return json(room.toJSON());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue