mirror of
https://git.suyu.dev/suyu/website.git
synced 2025-12-25 00:34:38 +01:00
remove concepts, finish lobbies/rooms
This commit is contained in:
parent
e498d512b4
commit
6bd72d56de
29 changed files with 235 additions and 822 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,4 +9,5 @@ node_modules
|
|||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
ssl
|
||||
*.sqlite
|
||||
*.sqlite
|
||||
src/lib/server/secrets
|
||||
38
package-lock.json
generated
38
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
|||
"sequelize": "^6.37.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-plugin-vsharp": "^1.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"express": "^4.18.3",
|
||||
"flowbite": "^2.3.0",
|
||||
|
|
@ -1593,6 +1595,12 @@
|
|||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
|
||||
|
|
@ -5640,6 +5648,14 @@
|
|||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sequelize/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
|
|
@ -6936,18 +6952,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
|
|
@ -7044,9 +7048,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"express": "^4.18.3",
|
||||
"flowbite": "^2.3.0",
|
||||
|
|
@ -51,6 +52,7 @@
|
|||
"sequelize": "^6.37.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-plugin-vsharp": "^1.7.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,73 @@
|
|||
import type { IRoom } from "$types/rooms";
|
||||
import type { IRoom, IRoomConfig, RoomPlayer } from "$types/rooms";
|
||||
import type { SuyuUser } from "../schema";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export class RoomManager {
|
||||
private static rooms: Room[] = [];
|
||||
static createRoom(room: IRoomConfig) {
|
||||
const newRoom = new Room(room);
|
||||
this.rooms.push(newRoom);
|
||||
return newRoom;
|
||||
}
|
||||
|
||||
static getRooms() {
|
||||
return this.rooms;
|
||||
}
|
||||
|
||||
static getRoom(id: string) {
|
||||
return this.rooms.find((room) => room.roomInfo.id === id);
|
||||
}
|
||||
|
||||
static removeRoom(id: string) {
|
||||
const index = this.rooms.findIndex((room) => room.roomInfo.id === id);
|
||||
this.rooms.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class Room {
|
||||
public roomInfo: IRoom;
|
||||
public host: SuyuUser;
|
||||
constructor(
|
||||
name: string,
|
||||
description: string,
|
||||
game: string,
|
||||
players: SuyuUser[],
|
||||
maxPlayers: number,
|
||||
// name: string,
|
||||
// description: string,
|
||||
// game: string,
|
||||
// players: SuyuUser[],
|
||||
// maxPlayers: number,
|
||||
// ip: string,
|
||||
config: IRoomConfig,
|
||||
) {
|
||||
const parsed = config.ip.split(":");
|
||||
this.host = config.host;
|
||||
this.roomInfo = {
|
||||
name,
|
||||
description,
|
||||
preferredGameName: game,
|
||||
players,
|
||||
maxPlayers,
|
||||
address: "localhost",
|
||||
externalGuid: "1234",
|
||||
hasPassword: false,
|
||||
id: "1234",
|
||||
name: config.name,
|
||||
description: config.description,
|
||||
preferredGameName: config.gameName,
|
||||
preferredGameId: config.gameId,
|
||||
players: config.players,
|
||||
maxPlayers: config.maxPlayers,
|
||||
address: parsed[0],
|
||||
externalGuid: v4(),
|
||||
hasPassword: config.hasPassword,
|
||||
id: v4(),
|
||||
netVersion: 1,
|
||||
owner: "1234",
|
||||
port: 1234,
|
||||
preferredGameId: 1234,
|
||||
owner: this.host.username,
|
||||
port: parseInt(parsed[1]),
|
||||
};
|
||||
}
|
||||
|
||||
async addPlayer(user: SuyuUser) {
|
||||
addPlayer(user: RoomPlayer) {
|
||||
this.roomInfo.players.push(user);
|
||||
}
|
||||
|
||||
setPlayerList(players: RoomPlayer[]) {
|
||||
this.roomInfo.players = players;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.roomInfo;
|
||||
}
|
||||
|
||||
delete() {
|
||||
RoomManager.removeRoom(this.roomInfo.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,30 @@
|
|||
import { userRepo } from "$lib/server/repo";
|
||||
import type { SuyuUser } from "$lib/server/schema";
|
||||
import { PUBLIC_KEY } from "$lib/server/secrets";
|
||||
import type { IJwtData } from "$types/auth";
|
||||
import cookie from "cookie";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export async function useAuth(request: Request | string): Promise<SuyuUser | null> {
|
||||
const apiKey =
|
||||
typeof request === "string"
|
||||
? request
|
||||
: cookie.parse(request.headers.get("cookie") || "").token;
|
||||
const cookies = cookie.parse(
|
||||
typeof request !== "string" ? request.headers.get("cookie") || "" : "",
|
||||
);
|
||||
const apiKey = typeof request === "string" ? request : cookies.token;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
if (apiKey.startsWith("Bearer ")) {
|
||||
const token = apiKey.replace("Bearer ", "");
|
||||
const decoded: IJwtData = jwt.verify(token, Buffer.from(PUBLIC_KEY), {
|
||||
algorithms: ["RS256"],
|
||||
}) as IJwtData;
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
id: decoded.id,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
apiKey,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
const excludedRoutesNav = ["/mockup/boot", "/mockup/w11"];
|
||||
const excludedRoutesBg = ["/mockup", "/mockup/w11"];
|
||||
console.log($page.url);
|
||||
$: isNavExcluded = excludedRoutesNav.some((route) => $page.url?.pathname.startsWith(route));
|
||||
$: isBgExcluded = excludedRoutesBg.some((route) => $page.url?.pathname === route);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
let base64Token: string;
|
||||
$: base64Token = data?.token ? btoa(data.token) : "";
|
||||
|
||||
let usernameToCreate: string;
|
||||
let createBtn: HTMLButtonElement;
|
||||
|
|
@ -50,7 +52,7 @@
|
|||
<p>
|
||||
{#if data?.token && data?.user && data.user.username}
|
||||
<p>Username: {data.user.username}</p>
|
||||
<p>Token: <code>{data.token}</code></p>
|
||||
<p>Token: <code>{base64Token}</code></p>
|
||||
{:else}
|
||||
<p>
|
||||
It appears you don't have an account; please register one to access suyu's online
|
||||
|
|
@ -97,6 +99,13 @@
|
|||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
user-select: all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.float-bottom-right {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,18 @@ import { promisify } from "util";
|
|||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
async function genKey(username: string) {
|
||||
const random = (await randomBytes(80)).toString("hex");
|
||||
let apiKey = `${username}:${random}`;
|
||||
let b64ApiKey = Buffer.from(apiKey).toString("base64");
|
||||
if (b64ApiKey.length > 80) {
|
||||
b64ApiKey = b64ApiKey.slice(0, 80);
|
||||
}
|
||||
// decode b64ApiKey
|
||||
apiKey = Buffer.from(b64ApiKey, "base64").toString("utf-8");
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const body: CreateAccountRequest = await request.json();
|
||||
if (!body.username) {
|
||||
|
|
@ -35,12 +47,14 @@ export async function POST({ request }) {
|
|||
error: "username already exists",
|
||||
});
|
||||
}
|
||||
// the api key can only be 80 characters total, including the username and colon
|
||||
const key = await genKey(body.username);
|
||||
const createdUser: SuyuUser = userRepo.create({
|
||||
username: body.username,
|
||||
avatarUrl: `https://avatars.githubusercontent.com/u/${Math.floor(Math.random() * 100000000)}?v=4`,
|
||||
displayName: body.username,
|
||||
roles: serializeRoles(["user"]),
|
||||
apiKey: `${body.username}:${(await randomBytes(32)).toString("hex")}`,
|
||||
apiKey: key,
|
||||
});
|
||||
await userRepo.save(createdUser);
|
||||
return json<CreateAccountResponse>({
|
||||
|
|
|
|||
5
src/routes/jwt/external/key.pem/+server.ts
vendored
5
src/routes/jwt/external/key.pem/+server.ts
vendored
|
|
@ -1,7 +1,6 @@
|
|||
import { json } from "$lib/server/util/index.js";
|
||||
|
||||
export function GET({ request }) {
|
||||
console.log(request);
|
||||
return new Response(
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
|
@ -43,6 +42,6 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
|||
);
|
||||
}
|
||||
|
||||
export function POST() {
|
||||
return new Response("god fucking kill me");
|
||||
export function POST({ request }) {
|
||||
return new Response();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { json } from "$lib/server/util/index.js";
|
||||
import { PRIVATE_KEY } from "$lib/server/secrets/index.js";
|
||||
import { useAuth } from "$lib/util/api/index.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export function GET({ request }) {
|
||||
console.log(request);
|
||||
return new Response(
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB.CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "text/html",
|
||||
},
|
||||
export async function POST({ request }) {
|
||||
const userKey = `${request.headers.get("x-username")}:${request.headers.get("x-token")}`;
|
||||
const user = await useAuth(userKey);
|
||||
const token = jwt.sign({ ...user }, Buffer.from(PRIVATE_KEY), { algorithm: "RS256" });
|
||||
return new Response(token, {
|
||||
headers: {
|
||||
"content-type": "text/html",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function POST() {
|
||||
return new Response("god fucking kill me");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,46 @@
|
|||
import { Room } from "$lib/server/class/Room";
|
||||
import { Room, RoomManager } from "$lib/server/class/Room";
|
||||
import { userRepo } from "$lib/server/repo/index.js";
|
||||
import { SuyuUser } from "$lib/server/schema";
|
||||
import { PUBLIC_KEY } from "$lib/server/secrets/index.js";
|
||||
import { json } from "$lib/server/util";
|
||||
import type { LobbyResponse } from "$types/rooms";
|
||||
import type { IJwtData } from "$types/auth.js";
|
||||
import type { IRoom, LobbyResponse } from "$types/rooms";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export async function GET({ request }) {
|
||||
return json<LobbyResponse>({
|
||||
rooms: [new Room("suyu Testing Room 1", "A testing room for suyu", "suyu", [], 4).toJSON()],
|
||||
rooms: RoomManager.getRooms().map((r) => r.toJSON()),
|
||||
});
|
||||
}
|
||||
|
||||
/* credit to janeberru for showing the shape of this data */
|
||||
export async function POST({ request, getClientAddress }) {
|
||||
// TODO: per-ip room limit
|
||||
const body: IRoom = await request.json();
|
||||
console.log(body);
|
||||
const token = request.headers.get("authorization")?.replace("Bearer ", "");
|
||||
if (!token) return new Response(null, { status: 401 });
|
||||
// TODO: jwt utils which type and validate automatically
|
||||
const data = jwt.verify(token, Buffer.from(PUBLIC_KEY), { algorithms: ["RS256"] }) as IJwtData;
|
||||
const user = await userRepo.findOne({ where: { id: data.id } });
|
||||
if (!user) return new Response(null, { status: 401 });
|
||||
const room = RoomManager.createRoom({
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
gameName: body.preferredGameName,
|
||||
gameId: body.preferredGameId,
|
||||
players: [
|
||||
{
|
||||
gameId: 0,
|
||||
gameName: "",
|
||||
nickname: user.username,
|
||||
},
|
||||
],
|
||||
maxPlayers: body.maxPlayers,
|
||||
ip: `${getClientAddress().split(":").at(-1)}:${body.port}`,
|
||||
host: user,
|
||||
hasPassword: body.hasPassword || false,
|
||||
});
|
||||
console.log(room.toJSON());
|
||||
return json(room.toJSON());
|
||||
}
|
||||
|
|
|
|||
28
src/routes/lobby/[id]/+server.ts
Normal file
28
src/routes/lobby/[id]/+server.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { RoomManager } from "$lib/server/class/Room";
|
||||
import { json } from "$lib/server/util/index.js";
|
||||
import { useAuth } from "$lib/util/api/index.js";
|
||||
|
||||
/* thanks again janeberru for the shape of this data */
|
||||
export async function POST({ request, params }) {
|
||||
const body = await request.json();
|
||||
const { id } = params;
|
||||
const room = RoomManager.getRoom(id);
|
||||
if (!room) return new Response(null, { status: 500 });
|
||||
const user = await useAuth(request.headers.get("authorization") || "");
|
||||
if (!user) return new Response(null, { status: 401 });
|
||||
if (user.id !== room.host.id) return new Response(null, { status: 401 });
|
||||
console.log(body.players);
|
||||
room.setPlayerList(body.players);
|
||||
return json({ message: "Lobby updated successfully" });
|
||||
}
|
||||
|
||||
export async function DELETE({ request, params }) {
|
||||
const { id } = params;
|
||||
const room = RoomManager.getRoom(id);
|
||||
if (!room) return new Response(null, { status: 500 });
|
||||
const user = await useAuth(request.headers.get("authorization") || "");
|
||||
if (!user) return new Response(null, { status: 401 });
|
||||
if (user.id !== room.host.id) return new Response(null, { status: 401 });
|
||||
room.delete();
|
||||
return json(room.toJSON());
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<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
|
||||
>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<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 +0,0 @@
|
|||
<h1>Multiplayer</h1>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<h1>Settings</h1>
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { json } from "$lib/server/util/index";
|
||||
|
||||
export function GET({ request }) {
|
||||
console.log(request.headers);
|
||||
return json({
|
||||
username: "aaa",
|
||||
username: "nullptr",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
8
src/types/auth.d.ts
vendored
Normal file
8
src/types/auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface IJwtData {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
roles: string;
|
||||
iat: number;
|
||||
}
|
||||
20
src/types/rooms.d.ts
vendored
20
src/types/rooms.d.ts
vendored
|
|
@ -10,12 +10,30 @@ export interface IRoom {
|
|||
name: string;
|
||||
netVersion: number;
|
||||
owner: string;
|
||||
players: SuyuUser[];
|
||||
players: RoomPlayer[];
|
||||
port: number;
|
||||
preferredGameId: number;
|
||||
preferredGameName: string;
|
||||
}
|
||||
|
||||
export interface IRoomConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
gameName: string;
|
||||
gameId: number;
|
||||
hasPassword: boolean;
|
||||
players: RoomPlayer[];
|
||||
maxPlayers: number;
|
||||
ip: string;
|
||||
host: SuyuUser;
|
||||
}
|
||||
|
||||
export interface RoomPlayer {
|
||||
gameId: number;
|
||||
gameName: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface LobbyResponse {
|
||||
rooms: IRoom[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue