Merge nullptr's github into master (#2)
This commit is contained in:
commit
7bf9825f63
33 changed files with 1389 additions and 95 deletions
25
README.md
25
README.md
|
@ -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
|
## 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
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
|
@ -27,12 +19,9 @@ npm run dev -- --open
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
To create a production version of your app:
|
To create a production version of our app, you can run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
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.
|
|
||||||
|
|
16
src/app.pcss
16
src/app.pcss
|
@ -41,6 +41,22 @@ h3 {
|
||||||
@apply outline-none ring-2 ring-sky-400;
|
@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 {
|
.cta-button {
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.06)),
|
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%);
|
radial-gradient(109.26% 109.26% at 49.83% 13.37%, #ffffff 0%, #babaca 100%);
|
||||||
|
|
BIN
src/assets/branding/tuta.png
Normal file
BIN
src/assets/branding/tuta.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/fonts/suivar.ttf
Normal file
BIN
src/assets/fonts/suivar.ttf
Normal file
Binary file not shown.
78
src/components/Card.svelte
Normal file
78
src/components/Card.svelte
Normal 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>
|
283
src/components/CardCarousel.svelte
Normal file
283
src/components/CardCarousel.svelte
Normal 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>
|
21
src/components/HomepageCounter.svelte
Normal file
21
src/components/HomepageCounter.svelte
Normal 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}
|
|
@ -52,7 +52,7 @@
|
||||||
{player.nickname}{#if player !== room.players[room.players.length - 1]},{" "}
|
{player.nickname}{#if player !== room.players[room.players.length - 1]},{" "}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if} | {room.hasPassword ? "Private" : "Public"} | {room.address}:{room.port}
|
{/if} | {room.hasPassword ? "Private" : "Public"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
5
src/lib/data/strings.json
Normal file
5
src/lib/data/strings.json
Normal 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
1
src/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
|
@ -4,7 +4,7 @@ import type { SuyuUser } from "../schema";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export class RoomManager {
|
export class RoomManager {
|
||||||
private static rooms: Room[] = [];
|
static rooms: Room[] = [];
|
||||||
static roomTimeout: Record<string, number> = {}; // room id, last heard from
|
static roomTimeout: Record<string, number> = {}; // room id, last heard from
|
||||||
static createRoom(room: IRoomConfig) {
|
static createRoom(room: IRoomConfig) {
|
||||||
const existingRoom = this.rooms.find((r) => r.host.username === room.host.username);
|
const existingRoom = this.rooms.find((r) => r.host.username === room.host.username);
|
||||||
|
|
|
@ -21,13 +21,22 @@ export async function useAuth(
|
||||||
const decoded: IJwtData = jwt.verify(token, Buffer.from(PUBLIC_KEY), {
|
const decoded: IJwtData = jwt.verify(token, Buffer.from(PUBLIC_KEY), {
|
||||||
algorithms: ["RS256"],
|
algorithms: ["RS256"],
|
||||||
}) as IJwtData;
|
}) as IJwtData;
|
||||||
const user = await userRepo.findOne({
|
let user = await userRepo.findOne({
|
||||||
where: {
|
where: {
|
||||||
apiKey: decoded.apiKey,
|
apiKey: decoded.apiKey,
|
||||||
},
|
},
|
||||||
loadEagerRelations: eager || false,
|
loadEagerRelations: eager || false,
|
||||||
relations: eager ? ["sentFriendRequests", "receivedFriendRequests"] : [],
|
relations: eager ? ["sentFriendRequests", "receivedFriendRequests"] : [],
|
||||||
});
|
});
|
||||||
|
if (!user) {
|
||||||
|
user = await userRepo.findOne({
|
||||||
|
where: {
|
||||||
|
id: decoded.id,
|
||||||
|
},
|
||||||
|
loadEagerRelations: eager || false,
|
||||||
|
relations: eager ? ["sentFriendRequests", "receivedFriendRequests"] : [],
|
||||||
|
});
|
||||||
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
const user = await userRepo.findOne({
|
const user = await userRepo.findOne({
|
||||||
|
|
|
@ -142,15 +142,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "FAQ",
|
name: "FAQ",
|
||||||
href: "/coming-soon",
|
href: "/faq",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Discord",
|
name: "Discord",
|
||||||
href: "https://discord.gg/suyu",
|
href: "https://discord.gg/suyu",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "GitLab",
|
name: "Git",
|
||||||
href: "https://gitlab.com/suyu-emu/suyu",
|
href: "https://git.suyu.dev/suyu/suyu",
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// name: $token || data.tokenCookie ? "Account" : "Sign up",
|
// 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">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">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>
|
||||||
<div class="flex w-full flex-row items-center justify-end text-[#A6A5A7]">
|
<div class="flex w-full flex-row items-center justify-end text-[#A6A5A7]">
|
||||||
<div class="flex flex-row gap-4 max-[625px]:hidden">
|
<div class="flex flex-row gap-4 max-[625px]:hidden">
|
||||||
<a
|
<a
|
||||||
class="p-2 transition hover:text-white"
|
class="p-2 transition hover:text-white"
|
||||||
href="https://gitlab.com/suyu-emu/suyu"
|
href="https://git.suyu.dev/suyu/suyu"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import embedImage from "$assets/branding/suyu__Embed-Image.png";
|
import embedImage from "$assets/branding/suyu__Embed-Image.png";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import suyuWindow from "$assets/mockups/suyuwindow.png";
|
import suyuWindow from "$assets/mockups/suyuwindow.png";
|
||||||
|
import HomepageCounter from "$components/HomepageCounter.svelte";
|
||||||
import { XCircleOutline } from "flowbite-svelte-icons";
|
import { XCircleOutline } from "flowbite-svelte-icons";
|
||||||
import { Dialog } from "radix-svelte";
|
import { Dialog } from "radix-svelte";
|
||||||
|
|
||||||
|
@ -70,18 +71,15 @@
|
||||||
</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="https://gitlab.com/suyu-emu/suyu/-/releases"
|
href="/download"
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
class="cta-button"
|
class="cta-button"
|
||||||
>
|
>
|
||||||
Download <svg
|
Download <svg
|
||||||
class=""
|
|
||||||
style="--icon-color:#000"
|
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="#000"
|
fill="currentColor"
|
||||||
role="img"
|
role="img"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -91,18 +89,16 @@
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://gitlab.com/suyu-emu/suyu"
|
href="https://git.suyu.dev/suyu/suyu"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
class="button text-[#8A8F98]"
|
class="button text-[#8A8F98]"
|
||||||
>
|
>
|
||||||
Contribute <svg
|
Contribute <svg
|
||||||
class=""
|
|
||||||
style="--icon-color:#8A8F98"
|
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="#8A8F98"
|
fill="currentColor"
|
||||||
role="img"
|
role="img"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
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%]"
|
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>
|
<h1 class="text-[48px] leading-[0.9]">By the numbers</h1>
|
||||||
<div class="flex flex-col gap-0">
|
<HomepageCounter count={contributors} subText="dedicated contributors" />
|
||||||
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
|
<HomepageCounter count={starCount} subText="GitLab stars" />
|
||||||
<!-- <AnimatedCounter
|
<HomepageCounter count={4000} subText="supported games" />
|
||||||
values={Array.from({ length: contributors + 1 }, (_, i) => i.toString())}
|
<HomepageCounter count={memberCount} subText="members on Discord" />
|
||||||
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>
|
|
||||||
</div>
|
</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] bg-[#110d10] lg:rounded-tl-none">
|
||||||
<div
|
<div
|
||||||
|
@ -233,14 +179,14 @@
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://gitlab.com/suyu-emu/suyu"
|
href="https://git.suyu.dev/suyu/suyu"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
class="relative w-full rounded-[2.25rem] bg-[#f78c40] p-12 text-black"
|
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">
|
<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.
|
to help us out, so feel free to check out our code.
|
||||||
</p>
|
</p>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
@ -3,6 +3,6 @@ import { RoomManager } from "$lib/server/class/Room.js";
|
||||||
export function load({ request }) {
|
export function load({ request }) {
|
||||||
const rooms = RoomManager.getRooms().map((r) => r.toJSON()) || [];
|
const rooms = RoomManager.getRooms().map((r) => r.toJSON()) || [];
|
||||||
return {
|
return {
|
||||||
rooms: rooms,
|
rooms: rooms.reverse(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
129
src/routes/download/+page.svelte
Normal file
129
src/routes/download/+page.svelte
Normal 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>
|
53
src/routes/faq/+page.svelte
Normal file
53
src/routes/faq/+page.svelte
Normal 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>
|
|
@ -41,7 +41,6 @@ export async function POST({ request, getClientAddress }) {
|
||||||
if (!token) return new Response(null, { status: 401 });
|
if (!token) return new Response(null, { status: 401 });
|
||||||
// TODO: jwt utils which type and validate automatically
|
// TODO: jwt utils which type and validate automatically
|
||||||
const user = await useAuth(token);
|
const user = await useAuth(token);
|
||||||
console.log(user);
|
|
||||||
if (!user) return new Response(null, { status: 401 });
|
if (!user) return new Response(null, { status: 401 });
|
||||||
const borkedIp = getClientAddress();
|
const borkedIp = getClientAddress();
|
||||||
const room = RoomManager.createRoom({
|
const room = RoomManager.createRoom({
|
||||||
|
@ -67,5 +66,11 @@ export async function POST({ request, getClientAddress }) {
|
||||||
hasPassword: body.hasPassword || false,
|
hasPassword: body.hasPassword || false,
|
||||||
});
|
});
|
||||||
console.log("Room added:", JSON.stringify(room, null, 2));
|
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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useAuth } from "$lib/util/api/index.js";
|
||||||
/* thanks again janeberru for the shape of this data */
|
/* thanks again janeberru for the shape of this data */
|
||||||
export async function POST({ request, params }) {
|
export async function POST({ request, params }) {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
console.log(body);
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const room = RoomManager.getRoom(id);
|
const room = RoomManager.getRoom(id);
|
||||||
if (!room) return new Response(null, { status: 500 });
|
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) return new Response(null, { status: 401 });
|
||||||
if (user.id !== room.host.id) 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) {
|
if (body.players.length === 0 && room.roomInfo.owner) {
|
||||||
console.log(room.roomInfo.players);
|
|
||||||
room.setPlayerList([{ gameId: 0, gameName: "", nickname: room.roomInfo.owner }]);
|
room.setPlayerList([{ gameId: 0, gameName: "", nickname: room.roomInfo.owner }]);
|
||||||
|
} else {
|
||||||
|
room.setPlayerList(body.players);
|
||||||
}
|
}
|
||||||
return json({ message: "Lobby updated successfully" });
|
return json({ message: "Lobby updated successfully" });
|
||||||
}
|
}
|
||||||
|
|
11
src/routes/mockup/boot/[game]/+page.server.ts
Normal file
11
src/routes/mockup/boot/[game]/+page.server.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
150
src/routes/mockup/boot/[game]/+page.svelte
Normal file
150
src/routes/mockup/boot/[game]/+page.svelte
Normal 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>
|
228
src/routes/mockup/w11/+page.svelte
Normal file
228
src/routes/mockup/w11/+page.svelte
Normal 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>
|
91
src/routes/mockup/w11/components/Card.svelte
Normal file
91
src/routes/mockup/w11/components/Card.svelte
Normal 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>
|
193
src/routes/mockup/w11/components/Sidebar.svelte
Normal file
193
src/routes/mockup/w11/components/Sidebar.svelte
Normal 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>
|
10
src/routes/mockup/w11/components/icons/Community.svelte
Normal file
10
src/routes/mockup/w11/components/icons/Community.svelte
Normal 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
|
||||||
|
>
|
10
src/routes/mockup/w11/components/icons/Globe.svelte
Normal file
10
src/routes/mockup/w11/components/icons/Globe.svelte
Normal 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
|
||||||
|
>
|
10
src/routes/mockup/w11/components/icons/Library.svelte
Normal file
10
src/routes/mockup/w11/components/icons/Library.svelte
Normal 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
|
||||||
|
>
|
10
src/routes/mockup/w11/components/icons/More.svelte
Normal file
10
src/routes/mockup/w11/components/icons/More.svelte
Normal 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
|
||||||
|
>
|
16
src/routes/mockup/w11/components/icons/QA.svelte
Normal file
16
src/routes/mockup/w11/components/icons/QA.svelte
Normal 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
|
||||||
|
>
|
10
src/routes/mockup/w11/components/icons/Settings.svelte
Normal file
10
src/routes/mockup/w11/components/icons/Settings.svelte
Normal 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
|
||||||
|
>
|
16
src/routes/mockup/w11/pages/Library.svelte
Normal file
16
src/routes/mockup/w11/pages/Library.svelte
Normal 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>
|
1
src/routes/mockup/w11/pages/Multiplayer.svelte
Normal file
1
src/routes/mockup/w11/pages/Multiplayer.svelte
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>Multiplayer</h1>
|
1
src/routes/mockup/w11/pages/Settings.svelte
Normal file
1
src/routes/mockup/w11/pages/Settings.svelte
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1>Settings</h1>
|
Loading…
Reference in a new issue