Initial commit
75
client/app/config.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import autoprefixer from "autoprefixer";
|
||||
import tailwindcss from "tailwindcss";
|
||||
import cssnano from "cssnano";
|
||||
import defaultTheme from "tailwindcss/defaultTheme";
|
||||
import forms from "@tailwindcss/forms";
|
||||
import typography from "@tailwindcss/typography";
|
||||
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
root: path.resolve(__dirname),
|
||||
base: "./",
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
tailwindcss(
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
{
|
||||
content: [path.resolve(__dirname, "src") + "/**/*.{html,js,svelte,ts}"],
|
||||
darkMode: ["selector", '[data-mode="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["'Source Sans 3'", ...defaultTheme.fontFamily.sans],
|
||||
mono: ["'Source Code Pro'", ...defaultTheme.fontFamily.mono],
|
||||
},
|
||||
colors: {
|
||||
aya: {
|
||||
100: "#e7d9fd",
|
||||
200: "#ceb3fb",
|
||||
300: "#b68cf9",
|
||||
400: "#9d66f7",
|
||||
500: "#8540f5",
|
||||
600: "#6a33c4",
|
||||
700: "#502693",
|
||||
800: "#351a62",
|
||||
900: "#1b0d31",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
extend: {
|
||||
backgroundColor: ["active", "focus"],
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [forms, typography],
|
||||
}
|
||||
),
|
||||
autoprefixer(),
|
||||
cssnano(),
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "../common/content/ui"),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: undefined,
|
||||
inlineDynamicImports: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [svelte()],
|
||||
});
|
||||
252
client/app/css/app.css
Normal file
@@ -0,0 +1,252 @@
|
||||
@import url("./fonts.css");
|
||||
@import url("./icons.css");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
@tailwind components;
|
||||
|
||||
@layer base {
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 100ms;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.aya-anim-pop:active:hover:not(:disabled),
|
||||
.aya-anim-pop:active:focus:not(:disabled) {
|
||||
transform: scale(0.97);
|
||||
animation: button-pop 0s ease-out;
|
||||
}
|
||||
|
||||
.aya-page-btn {
|
||||
@apply aya-anim-pop flex cursor-default items-center justify-center duration-100;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border: 1px solid transparent;
|
||||
transition-property: border, border-top, background-color, border-bottom;
|
||||
}
|
||||
|
||||
.aya-page-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.aya-page-btn-selected {
|
||||
@apply aya-page-btn;
|
||||
background-color: rgba(255, 255, 255);
|
||||
border: 1px solid rgba(0, 0, 0, 0.065) !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.122) !important;
|
||||
}
|
||||
|
||||
.aya-page-btn-selected:hover {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.065) !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.105) !important;
|
||||
}
|
||||
|
||||
[data-mode="dark"] {
|
||||
.aya-page-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid transparent !important;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
|
||||
}
|
||||
|
||||
.aya-page-btn-selected {
|
||||
background-color: rgba(255, 255, 255, 0.045);
|
||||
border: 1px solid rgba(255, 255, 255, 0.03) !important;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.07) !important;
|
||||
}
|
||||
|
||||
.aya-page-btn-selected:hover {
|
||||
background-color: rgba(255, 255, 255, 0.075);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.025) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hue-rotate {
|
||||
0% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
filter: hue-rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.aya-bg-rainbow {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aya-bg-rainbow::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(45deg, theme("colors.indigo.500") 0%, theme("colors.red.500") 100%);
|
||||
animation: hue-rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
.aya-bg-rainbow > * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.aya-anim-pop:active:hover:not(:disabled),
|
||||
.aya-anim-pop:active:focus:not(:disabled) {
|
||||
animation: button-pop 0.25s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.aya-btn-sm {
|
||||
@apply !h-8 !min-h-8 !px-3 !text-base;
|
||||
}
|
||||
|
||||
.aya-btn-xs {
|
||||
@apply !h-6 !min-h-6 !px-2 !text-sm;
|
||||
}
|
||||
|
||||
.aya-btn-lg {
|
||||
@apply !h-[3rem] !min-h-[3rem] !px-4;
|
||||
}
|
||||
|
||||
.aya-btn-green {
|
||||
@apply !bg-green-600/85 hover:!bg-green-600;
|
||||
}
|
||||
|
||||
.aya-btn-link-sm {
|
||||
@apply !px-1 !py-0;
|
||||
}
|
||||
|
||||
.aya-btn-alt {
|
||||
@apply !bg-aya-700 hover:!bg-aya-800;
|
||||
}
|
||||
|
||||
/* AppLayout */
|
||||
.aya-nav-link {
|
||||
@apply aya-anim-pop me-1 rounded-lg px-3 py-2 text-white text-opacity-50 transition duration-200 hover:bg-white/10 hover:text-opacity-100 hover:shadow-sm focus:text-white active:bg-white/20 dark:hover:bg-black/20;
|
||||
}
|
||||
|
||||
.aya-nav-link:not(.aya-nav-link-selected) {
|
||||
@apply focus:bg-aya-400/75 dark:focus:bg-aya-600/75;
|
||||
}
|
||||
|
||||
.aya-nav-link-sm {
|
||||
@apply aya-anim-pop me-1 rounded-lg px-2 py-1 text-white text-opacity-50 transition duration-200 hover:bg-white/5 hover:text-opacity-100 focus:bg-white/10 focus:text-white active:bg-white/10;
|
||||
}
|
||||
|
||||
.aya-nav-link-selected {
|
||||
@apply bg-aya-600/75 text-opacity-100 hover:bg-aya-600 dark:bg-aya-600/50 dark:hover:bg-aya-600/75;
|
||||
}
|
||||
|
||||
.aya-nav-link-sm.aya-nav-link-selected {
|
||||
@apply bg-white/10 text-opacity-100;
|
||||
}
|
||||
|
||||
.aya-footer-link {
|
||||
@apply aya-anim-pop flex-grow rounded-lg bg-transparent px-2 py-2 text-center text-xl opacity-35 transition duration-200 hover:bg-white hover:bg-opacity-10 hover:opacity-100 hover:shadow-md;
|
||||
}
|
||||
|
||||
.aya-games-genre-list {
|
||||
@apply aya-anim-pop ms-3 rounded-md px-2 py-0.5 text-start text-neutral-600 transition duration-200 hover:bg-black/10 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-200;
|
||||
}
|
||||
|
||||
.aya-games-sort-list {
|
||||
@apply aya-anim-pop ms-3 rounded-md px-2 py-0.5 text-start text-neutral-600 transition duration-200 hover:bg-black/10 dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-200;
|
||||
}
|
||||
|
||||
.aya-limited-label,
|
||||
.aya-limited-unique-label,
|
||||
.aya-limited-label-lg,
|
||||
.aya-limited-unique-label-lg {
|
||||
background-image: url("../img/itemlabels.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-size: auto auto;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.aya-limited-label,
|
||||
.aya-limited-unique-label {
|
||||
width: 70px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.aya-limited-unique-label {
|
||||
background-position: 0 -18px;
|
||||
}
|
||||
|
||||
.aya-limited-label {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.aya-limited-label-lg,
|
||||
.aya-limited-unique-label-lg {
|
||||
width: 105px;
|
||||
height: 22px;
|
||||
background-size: 105px auto;
|
||||
}
|
||||
|
||||
.aya-limited-unique-label-lg {
|
||||
background-position: 0 -26px;
|
||||
}
|
||||
|
||||
.aya-limited-label-lg {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.aya-character-body-selector div {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* character body */
|
||||
/* ugly! */
|
||||
.body-head {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
margin-left: 72px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.body-torso {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.body-left-arm,
|
||||
.body-right-arm {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.body-left-leg,
|
||||
.body-right-leg {
|
||||
display: inline-block;
|
||||
width: 49px !important;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.body-left-arm,
|
||||
.body-right-arm {
|
||||
width: 49px;
|
||||
}
|
||||
|
||||
.body-left-arm,
|
||||
.body-right-arm,
|
||||
.body-left-leg,
|
||||
.body-right-leg {
|
||||
height: 100px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.body-left-leg {
|
||||
margin-left: 52px;
|
||||
}
|
||||
}
|
||||
253
client/app/css/fonts.css
Normal file
71
client/app/css/icons.css
Normal file
25
client/app/index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US" class="bg-white dark:bg-neutral-900">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link rel="icon" href="./img/aya.png" />
|
||||
<title>Aya</title>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script type="module" src="./main.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.documentElement.setAttribute("data-mode", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-mode", "light");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
13
client/app/main.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import "./css/app.css"
|
||||
import App from "./src/App.svelte"
|
||||
import { mount } from "svelte";
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const mode = params.get("mode") || "player" // can be of [player, studio, server] to access 3 different UIs
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app"),
|
||||
props: { mode }
|
||||
})
|
||||
|
||||
export default app
|
||||
BIN
client/app/public/img/aya-server-error.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
client/app/public/img/aya-server-progress.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
client/app/public/img/aya-server.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
client/app/public/img/aya-studio.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
client/app/public/img/aya.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
client/app/public/img/devs/3d_glasses.png
Normal file
|
After Width: | Height: | Size: 471 B |
BIN
client/app/public/img/devs/angel.png
Normal file
|
After Width: | Height: | Size: 636 B |
BIN
client/app/public/img/devs/bomb.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
client/app/public/img/devs/bricks.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
client/app/public/img/devs/cat.png
Normal file
|
After Width: | Height: | Size: 809 B |
BIN
client/app/public/img/devs/heart.png
Normal file
|
After Width: | Height: | Size: 570 B |
BIN
client/app/public/img/devs/weed.png
Normal file
|
After Width: | Height: | Size: 392 B |
BIN
client/app/public/img/icons/avatar.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
client/app/public/img/icons/extensions.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
client/app/public/img/icons/favorites.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
client/app/public/img/icons/host.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
client/app/public/img/icons/item.png
Normal file
|
After Width: | Height: | Size: 613 B |
BIN
client/app/public/img/icons/level.png
Normal file
|
After Width: | Height: | Size: 928 B |
BIN
client/app/public/img/icons/levels.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
client/app/public/img/icons/model.png
Normal file
|
After Width: | Height: | Size: 796 B |
BIN
client/app/public/img/icons/packages.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
client/app/public/img/icons/play.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
client/app/public/img/icons/server.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
client/app/public/img/icons/studio.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
client/app/public/img/mega.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
client/app/public/img/pbs.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
24
client/app/src/App.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import PlayerApp from "@/Player.svelte"
|
||||
import StudioApp from "@/Studio.svelte"
|
||||
import ServerApp from "@/Server.svelte"
|
||||
|
||||
let { mode } = $props();
|
||||
|
||||
let transport = $state(null)
|
||||
|
||||
onMount(() => {
|
||||
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||
transport = channel.objects.transport
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if mode === "player"}
|
||||
<PlayerApp {transport} />
|
||||
{:else if mode === "studio"}
|
||||
<StudioApp {transport} />
|
||||
{:else if mode === "server"}
|
||||
<ServerApp {transport} />
|
||||
{/if}
|
||||
20
client/app/src/Components/Card.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
let { title = null, icon = null, class: className = "", children, ...restProps } = $props()
|
||||
|
||||
let classes = [className, "rounded-md border border-slate-200 bg-gray-50 dark:border-neutral-700 dark:bg-neutral-800/80"].filter(Boolean).join(" ")
|
||||
</script>
|
||||
|
||||
<div class={classes} {...restProps}>
|
||||
{#if title === null && children?.title}
|
||||
{@render children.title()}
|
||||
{:else if title !== null}
|
||||
<div class="flex w-full select-none items-center rounded-t-md border-b border-slate-200 bg-gray-100 px-2 py-1 font-semibold dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-50">
|
||||
<i class="fa-regular fa-light fa-fw {icon} me-1" />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="p-3">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
11
client/app/src/Components/PlayerPageButton.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
let { page, selectedPage, onClick } = $props();
|
||||
</script>
|
||||
|
||||
<button onclick={onClick} data-page={page.id} title={page.title} class="mb-1.5 rounded-lg p-2 {page.id == selectedPage ? 'aya-page-btn-selected' : 'aya-page-btn'}">
|
||||
{#if page.id == "about" || page.id == "settings"}
|
||||
<i class="{page.id == selectedPage ? 'fa-solid' : 'fa-light'} {page.icon} fa-fw fa-lg text-zinc-800 dark:text-neutral-50"></i>
|
||||
{:else}
|
||||
<img src="./img/icons/{page.id}.png" alt={page.title} />
|
||||
{/if}
|
||||
</button>
|
||||
36
client/app/src/Components/ServerBrowser.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import Card from "@/Components/Card.svelte"
|
||||
import Button from "@/Controls/Button.svelte";
|
||||
import InputError from "@/Controls/InputError.svelte"
|
||||
|
||||
let transport
|
||||
let servers = $state([]);
|
||||
let error = $state("");
|
||||
|
||||
onMount(() => {
|
||||
new QWebChannel(qt.webChannelTransport, (channel) => {
|
||||
transport = channel.objects.jsHelpers
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<Card class="col-span-1 h-full" icon="fa-server">
|
||||
|
||||
{#if error}
|
||||
<InputError message={error} />
|
||||
{/if}
|
||||
|
||||
{#each servers as server, i}
|
||||
<div class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-3 py-1.5">
|
||||
<div class="w-full">
|
||||
<span class="text-gray-600">({server.player_count}/{server.player_limit})</span>
|
||||
<span class="font-semibold">{server.host}</span>'s {server.server_name} {i}
|
||||
<Button text="Join" class="ms-1 aya-btn-sm" onclick={() => joinServer(i)} />
|
||||
</div>
|
||||
<!-- <img class="ms-1" height="10" src="./img/pbs.png" /> -->
|
||||
<!-- <img class="ms-1" height="10" src="./img/mega.png" /> -->
|
||||
</div>
|
||||
{/each}
|
||||
</Card>
|
||||
8
client/app/src/Components/ServerPageButton.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
let { page, selectedPage, onClick } = $props();
|
||||
</script>
|
||||
|
||||
<button onclick={onClick} data-page={page.id} title={page.title} class="flex min-h-12 min-w-12 flex-col items-center rounded {page.id == selectedPage ? 'aya-page-btn-selected text-neutral-600 dark:text-gray-200' : 'aya-page-btn text-neutral-500 dark:text-gray-500'}">
|
||||
<i class="{page.id == selectedPage ? 'fa-solid' : 'fa-light'} {page.icon} fa-fw"></i>
|
||||
<span class="mt-1 text-xs {page.id == selectedPage ? 'font-semibold' : 'font-medium'}">{page.title}</span>
|
||||
</button>
|
||||
6
client/app/src/Controls/Breadcrumb.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
let { class: className = "", children } = $props();
|
||||
</script>
|
||||
<div class="{className} mb-3 flex w-full rounded-lg bg-gray-200 dark:bg-zinc-800 px-4 py-2">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
18
client/app/src/Controls/BreadcrumbItem.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {boolean} [active]
|
||||
* @property {import('svelte').Snippet} [children]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { active = false, children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="dark:text-white {active ? '' : 'me-2'}">
|
||||
{@render children?.()}
|
||||
|
||||
{#if !active}
|
||||
<i class="fa-light fa-chevron-right fa-fw text-neutral-500"></i>
|
||||
{/if}
|
||||
</div>
|
||||
41
client/app/src/Controls/Button.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
let { icon = null, text = null, disabled = false, class: className = "", onclick = null, children, ...restProps } = $props()
|
||||
|
||||
const iconStore = writable(icon)
|
||||
|
||||
onMount(() => iconStore.set(`fa-regular ${icon}`))
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!disabled) iconStore.set(`fa-solid ${icon}`)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!disabled) iconStore.set(`fa-regular ${icon}`)
|
||||
}
|
||||
|
||||
const handleMouseClick = (e) => {
|
||||
if (!disabled && onclick) onclick(e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="aya-anim-pop inline-flex h-10 min-h-10 items-center justify-center gap-2 rounded-lg bg-aya-500 px-3 text-lg text-white transition duration-200 hover:bg-aya-600 disabled:opacity-35 {className}"
|
||||
onmouseenter={handleMouseEnter}
|
||||
onmouseleave={handleMouseLeave}
|
||||
onclick={handleMouseClick}
|
||||
disabled={disabled || null}
|
||||
{...restProps}
|
||||
>
|
||||
{#if icon}
|
||||
<i class={$iconStore} />
|
||||
{/if}
|
||||
|
||||
{#if text}
|
||||
<span>{text}</span>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</button>
|
||||
11
client/app/src/Controls/ButtonLink.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
let { href = null, text = null, class: className = "", children, ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<button href={href} class="aya-anim-pop rounded-lg px-2 py-1 text-neutral-500 underline transition duration-200 hover:bg-black/10 hover:text-neutral-600 dark:text-zinc-500 dark:hover:bg-white/10 dark:hover:text-zinc-400 {className}" {...restProps}>
|
||||
{#if !text}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{text}
|
||||
{/if}
|
||||
</button>
|
||||
22
client/app/src/Controls/Checkbox.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
let { checked = $bindable(false), onchange = null, label = null, labelClass = "", class: className = "", children, ...restProps } = $props()
|
||||
|
||||
const handleChange = () => {
|
||||
if (onchange) onchange(checked)
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked
|
||||
onchange={handleChange}
|
||||
class="aya-anim-pop cursor-pointer rounded border-gray-300 text-aya-500 shadow-sm transition duration-100 focus:ring-aya-400 dark:border-zinc-700 dark:bg-zinc-900 {className}"
|
||||
{...restProps}
|
||||
/>
|
||||
{#if label}
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300 {labelClass}">{label}</span>
|
||||
{:else}
|
||||
<span class="ml-2 flex items-center text-gray-700 dark:text-gray-300 {labelClass}">{@render children?.()}</span>
|
||||
{/if}
|
||||
</label>
|
||||
48
client/app/src/Controls/Dropdown.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
|
||||
import DropdownButton from "@/Controls/DropdownButton.svelte"
|
||||
|
||||
let { button = null, class: className = "", children, ...restProps } = $props();
|
||||
|
||||
let shown = $state(false)
|
||||
let dropdownRef = $state()
|
||||
|
||||
setContext("dropdown", {
|
||||
close: () => (shown = false),
|
||||
isShown: () => shown
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef && !dropdownRef.contains(event.target) && shown) {
|
||||
shown = false
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("click", handleClickOutside)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", handleClickOutside)
|
||||
}
|
||||
})
|
||||
|
||||
const toggleDropdown = () => (shown = !shown)
|
||||
</script>
|
||||
|
||||
<div bind:this={dropdownRef} class="relative" {...restProps}>
|
||||
{#if button}
|
||||
{@render button?.({ shown, toggleDropdown })}
|
||||
{:else}
|
||||
<DropdownButton {shown} onclick={toggleDropdown} />
|
||||
{/if}
|
||||
|
||||
{#if shown}
|
||||
<div class="absolute z-10 mt-2 w-48 rounded-md bg-white dark:bg-zinc-800 shadow-lg ring-1 ring-black ring-opacity-5 {className}" transition:fade={{ duration: 50 }}>
|
||||
<div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
9
client/app/src/Controls/DropdownButton.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
let { shown = false, onclick = () => {}, children } = $props();
|
||||
</script>
|
||||
|
||||
<button {onclick} class="aya-anim-pop rounded-lg text-neutral-500 transition duration-200 hover:text-neutral-600 {shown ? 'bg-black/10' : 'hover:bg-black/5'}">
|
||||
{#if children}{@render children({ shown })}{:else}
|
||||
<i class="{shown ? 'fa-solid' : 'fa-regular'} fa-caret-down fa-fw aya-anim-pop"></i>
|
||||
{/if}
|
||||
</button>
|
||||
1
client/app/src/Controls/DropdownDivider.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<div class="w-100 my-1 border-t dark:border-neutral-700"></div>
|
||||
27
client/app/src/Controls/DropdownItem.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
let { text = null, icon = null, iconPosition = "left", style = null, selected = false, onclick = null, class: className = "", ...restProps } = $props()
|
||||
|
||||
const { close } = getContext("dropdown")
|
||||
|
||||
function handleClick() {
|
||||
if (onclick) onclick()
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={handleClick}
|
||||
class="aya-anim-pop flex w-full items-center px-3 py-1 text-sm text-neutral-700 transition duration-100 dark:text-neutral-300 {style === 'danger' ? 'hover:bg-red-100 hover:text-red-900 dark:hover:bg-red-950 dark:hover:text-red-100' : 'hover:bg-gray-100 dark:hover:bg-zinc-700'} {icon && iconPosition === 'right' ? 'justify-between' : ''} {selected ? 'bg-gray-200 hover:bg-gray-300' : ''} {className}"
|
||||
role="menuitem"
|
||||
{...restProps}
|
||||
>
|
||||
{#if icon && iconPosition === "left"}
|
||||
<i class="{selected ? 'fa-solid' : 'fa-light'} {icon} fa-fw me-1" />
|
||||
{/if}
|
||||
<span class={selected ? "font-semibold" : "font-normal"}>{text}</span>
|
||||
{#if icon && iconPosition === "right"}
|
||||
<i class="{selected ? 'fa-solid' : 'fa-light'} {icon} fa-fw" />
|
||||
{/if}
|
||||
</button>
|
||||
15
client/app/src/Controls/InputError.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {any} [message]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { message = null } = $props();
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<p class="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||
{message}
|
||||
</p>
|
||||
{/if}
|
||||
11
client/app/src/Controls/InputLabel.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
let { value = null, for: forAttr = null, children, ...restProps } = $props()
|
||||
</script>
|
||||
|
||||
<label class="select-none dark:text-neutral-100" for={forAttr} {...restProps}>
|
||||
{#if value}
|
||||
<span>{value}</span>
|
||||
{:else}
|
||||
<span>{@render children?.()}</span>
|
||||
{/if}
|
||||
</label>
|
||||
25
client/app/src/Controls/List.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
let { items = [], activeItem = $bindable(items[0]?.id), onitemchange = null } = $props();
|
||||
|
||||
function handleItemChange(id) {
|
||||
activeItem = id
|
||||
if (onitemchange) onitemchange(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#each items as item}
|
||||
{#if activeItem === item.id}
|
||||
<div class="flex items-center">
|
||||
<button class="aya-games-genre-list w-full !ms-0"><i class={`fa-solid fa-caret-right me-1 text-red-500`}></i> {item.label}</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button class={`aya-games-genre-list ${!item.icon ? 'ml-[10px]' : ''}`} onclick={() => handleItemChange(item.id)}>
|
||||
{#if item.icon}
|
||||
<i class={`fa-light fa-fw me-1 ${item.icon}`}></i>
|
||||
{/if}
|
||||
{item.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
15
client/app/src/Controls/ListButtons.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
let { items = [], activeItem = $bindable(items[0]?.id), onitemchange = null } = $props();
|
||||
|
||||
function handleItemChange(id) {
|
||||
activeItem = id
|
||||
if (onitemchange) onitemchange(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each items as item}
|
||||
<button class="aya-anim-pop flex flex-nowrap items-center rounded-lg px-2 py-1 text-start transition duration-200 {activeItem === item.id ? 'bg-black/10 dark:bg-white/10 font-semibold text-aya-700 dark:text-aya-500' : 'text-aya-600/85 hover:bg-black/5 dark:hover:bg-white/5 hover:text-aya-600 dark:text-aya-600'}" class:active={activeItem === item.id} onclick={() => handleItemChange(item.id)}>
|
||||
<i class={`${activeItem === item.id ? "fa-solid" : "fa-regular"} ${item.icon} fa-fw me-1`}></i>
|
||||
{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
37
client/app/src/Controls/Pagination.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import Button from "@/Controls/Button.svelte"
|
||||
|
||||
let { currentPage, lastPage, routeName } = $props();
|
||||
|
||||
const pageNumbers = () => {
|
||||
let pages = []
|
||||
const maxPages = 10
|
||||
let start = Math.max(currentPage - Math.floor(maxPages / 2), 1)
|
||||
let end = Math.min(start + maxPages - 1, lastPage)
|
||||
|
||||
if (end - start < maxPages - 1) {
|
||||
start = Math.max(end - maxPages + 1, 1)
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
const goToPage = (pageNumber) => {
|
||||
if (pageNumber < 1 || pageNumber > lastPage) return
|
||||
window.location.href = window.route(routeName, { page: pageNumber })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if lastPage > 1}
|
||||
<div class="mt-4 flex items-center justify-center space-x-0">
|
||||
<Button class="aya-btn-sm rounded-l-md rounded-r-none border hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700" text="‹" disabled={currentPage === 1} onclick={() => goToPage(currentPage - 1)} />
|
||||
{#each pageNumbers() as pageNum}
|
||||
<Button class={`aya-btn-sm border ${pageNum === currentPage ? "text-gray-700" : "hover:bg-gray-100"} ${pageNum === currentPage ? "cursor-not-allowed" : ""} rounded-none dark:border-gray-600 dark:hover:bg-gray-700`} text={pageNum} disabled={pageNum === currentPage} onclick={() => pageNum !== currentPage && goToPage(pageNum)} />
|
||||
{/each}
|
||||
<Button class="aya-btn-sm rounded-l-none rounded-r-md border hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700" text="›" disabled={currentPage === lastPage} onclick={() => goToPage(currentPage + 1)} />
|
||||
</div>
|
||||
{/if}
|
||||
17
client/app/src/Controls/PillButtons.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
let { items = [], activeItem = $bindable(items[0]?.id), onitemchange = null } = $props();
|
||||
|
||||
function handleItemChange(id) {
|
||||
activeItem = id
|
||||
if (onitemchange) onitemchange({ id })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
{#each items as item}
|
||||
<button class="aya-anim-pop me-1 rounded-full px-3 py-1 text-sm transition duration-200 {activeItem === item.id ? 'border border-aya-500 bg-white font-medium text-aya-500 hover:bg-black/5 dark:border-aya-400 dark:bg-neutral-800 dark:text-aya-400' : 'text-neutral-600 hover:bg-black/10 dark:text-neutral-400 dark:hover:bg-white/10'}" class:active={activeItem === item.id} onclick={() => handleItemChange(item.id)}>
|
||||
<i class={`${activeItem === item.id ? "fa-solid" : "fa-regular"} ${item.icon} fa-fw`}></i>
|
||||
{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
14
client/app/src/Controls/Select.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [value]
|
||||
* @property {import('svelte').Snippet} [children]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { value = $bindable(""), children } = $props();
|
||||
</script>
|
||||
|
||||
<select bind:value class="mt-1 w-full rounded-lg dark:bg-zinc-900 border-gray-300 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300 transition duration-100 focus:border-aya-500">
|
||||
{@render children?.()}
|
||||
</select>
|
||||
71
client/app/src/Controls/Tabs.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let { tabs = [], activeTab = $bindable(), hideTabs = false, roundedCorners = true, ontabchange = null, class: className = "", ...restProps } = $props()
|
||||
|
||||
let tabRefs = {}
|
||||
let lineStyle = $state("")
|
||||
|
||||
function setActiveTab(id) {
|
||||
activeTab = id
|
||||
if (ontabchange) ontabchange(id)
|
||||
updateLine()
|
||||
}
|
||||
|
||||
function updateLine() {
|
||||
const activeTabRef = tabRefs[activeTab]
|
||||
if (activeTabRef) {
|
||||
lineStyle = `left: ${activeTabRef.offsetLeft}px; width: ${activeTabRef.offsetWidth}px;`
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
updateLine()
|
||||
}
|
||||
|
||||
let tabClasses = $derived(tabs.map((tab, index) => {
|
||||
let classes = ["aya-anim-pop", "px-6", "py-2", "text-lg", "transition", "duration-200", "hover:bg-black/5", "dark:hover:bg-white/5", className]
|
||||
|
||||
if (activeTab === tab.id) {
|
||||
classes.push("font-medium", "text-aya-500")
|
||||
} else {
|
||||
classes.push("text-neutral-500", "dark:text-neutral-400", "hover:text-neutral-700", "dark:hover:text-neutral-300")
|
||||
}
|
||||
|
||||
if (roundedCorners) {
|
||||
classes.push("rounded-t-lg")
|
||||
} else {
|
||||
if (index === 0) classes.push("rounded-tl-lg")
|
||||
if (index === tabs.length - 1) classes.push("rounded-tr-lg")
|
||||
if (index !== 0 && index !== tabs.length - 1) classes.push("border-t-0")
|
||||
}
|
||||
|
||||
return classes.join(" ")
|
||||
}))
|
||||
|
||||
onMount(() => {
|
||||
updateLine()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
activeTab
|
||||
hideTabs
|
||||
updateLine()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window onresize={handleResize} />
|
||||
|
||||
<div class="relative mb-2 flex {className}" {...restProps}>
|
||||
{#each tabs as tab, index}
|
||||
<button bind:this={tabRefs[tab.id]} onclick={() => setActiveTab(tab.id)} class={tabClasses[index]}>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
{#if !hideTabs}
|
||||
<span class="absolute bottom-0 h-0.5 rounded bg-aya-500 transition-all duration-200 ease-in-out" style={lineStyle}></span>
|
||||
{/if}
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
<slot {activeTab}></slot>
|
||||
25
client/app/src/Controls/TextArea.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let { value = $bindable(""), error = null, class: className = "", ...restProps } = $props()
|
||||
|
||||
let textAreaElement
|
||||
let isFocused = $state(false)
|
||||
|
||||
onMount(() => {
|
||||
if (textAreaElement.hasAttribute("autofocus")) {
|
||||
textAreaElement.focus()
|
||||
}
|
||||
})
|
||||
|
||||
let classes = $derived([error ? "pe-7 dark:border-red-400 border-red-600 focus:border-red-500 focus:ring-red-600 hover:border-red-400 dark:hover:border-red-600" : "border-gray-300 dark:border-gray-700 hover:border-aya-300 focus:border-aya-500 dark:border-zinc-700 dark:focus:border-aya-600", className, "focus:ring-aya-500 block w-full rounded-lg border-gray-300 transition duration-100 dark:bg-zinc-900 dark:text-gray-300"].filter(Boolean).join(" "))
|
||||
|
||||
let iconColor = $derived(isFocused ? "text-red-500 dark:text-red-600" : "text-red-600 dark:text-red-400")
|
||||
</script>
|
||||
|
||||
<div class="relative {className && className.includes('w-full') ? 'w-full' : ''}">
|
||||
<textarea class={classes} bind:value bind:this={textAreaElement} onfocus={() => (isFocused = true)} onblur={() => (isFocused = false)} {...restProps} />
|
||||
{#if error}
|
||||
<i class={`fas fa-times pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 transform transition duration-100 ${iconColor}`} />
|
||||
{/if}
|
||||
</div>
|
||||
28
client/app/src/Controls/TextInput.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let { value = $bindable(""), error = null, icon = null, class: className = "", ...restProps } = $props()
|
||||
|
||||
let inputElement
|
||||
let isFocused = $state(false)
|
||||
|
||||
onMount(() => {
|
||||
if (inputElement.hasAttribute("autofocus")) {
|
||||
inputElement.focus()
|
||||
}
|
||||
})
|
||||
|
||||
let classes = $derived([error ? "pe-7 dark:border-red-400 border-red-600 focus:border-red-500 focus:ring-red-600 hover:border-red-400 dark:hover:border-red-600" : "border-gray-300 hover:border-aya-300 focus:border-aya-500 dark:border-neutral-700 dark:focus:border-aya-600", `focus:ring-aya-500 block w-full rounded border-gray-300 transition duration-100 dark:bg-neutral-900 dark:text-neutral-300 h-[2rem] dark:placeholder-neutral-500 ${className ?? ""} ${icon ? "pl-10" : ""}`].filter(Boolean).join(" "))
|
||||
|
||||
let iconColor = $derived(isFocused ? "text-red-500 dark:text-red-600" : "text-red-600 dark:text-red-400")
|
||||
</script>
|
||||
|
||||
<div class="relative {className && className.includes('w-full') ? 'w-full' : ''}">
|
||||
{#if icon}
|
||||
<i class={`fa-regular fa-fw ${icon} pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400`} />
|
||||
{/if}
|
||||
<input class={classes} bind:value bind:this={inputElement} onfocus={() => (isFocused = true)} onblur={() => (isFocused = false)} {...restProps} />
|
||||
{#if error}
|
||||
<i class={`fa-solid fa-times pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 transform transition duration-100 ${iconColor}`} />
|
||||
{/if}
|
||||
</div>
|
||||
7
client/app/src/Controls/TextLink.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let { href = null, text = null, class: className = "", ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<button href={href} class="aya-anim-pop rounded-lg text-aya-500 underline transition duration-200 hover:text-aya-400 {className}" {...restProps}>
|
||||
{text}
|
||||
</button>
|
||||
17
client/app/src/Enums/ChatStyle.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const ChatStyle = {
|
||||
Classic: 0,
|
||||
Bubble: 1,
|
||||
ClassicAndBubble: 2
|
||||
}
|
||||
|
||||
const names = {
|
||||
[ChatStyle.Classic]: "Classic",
|
||||
[ChatStyle.Bubble]: "Bubble",
|
||||
[ChatStyle.ClassicAndBubble]: "Classic and Bubble"
|
||||
}
|
||||
|
||||
function getChatStyleName(style) {
|
||||
return names[style]
|
||||
}
|
||||
|
||||
export { ChatStyle, getChatStyleName }
|
||||
57
client/app/src/Enums/GearType.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const GearType = {
|
||||
MeleeWeapons: 0,
|
||||
RangedWeapons: 1,
|
||||
Explosives: 2,
|
||||
PowerUps: 3,
|
||||
NavigationEnhancers: 4,
|
||||
MusicalInstruments: 5,
|
||||
SocialItems: 6,
|
||||
BuildingTools: 7,
|
||||
Transport: 8
|
||||
}
|
||||
|
||||
const names = {
|
||||
[GearType.MeleeWeapons]: "Melee Weapons",
|
||||
[GearType.RangedWeapons]: "Ranged Weapons",
|
||||
[GearType.Explosives]: "Explosives",
|
||||
[GearType.PowerUps]: "Power Ups",
|
||||
[GearType.NavigationEnhancers]: "Navigation Enhancers",
|
||||
[GearType.MusicalInstruments]: "Musical Instruments",
|
||||
[GearType.SocialItems]: "Social Items",
|
||||
[GearType.BuildingTools]: "Building Tools",
|
||||
[GearType.Transport]: "Transport"
|
||||
}
|
||||
|
||||
const icons = {
|
||||
[GearType.MeleeWeapons]: "sword",
|
||||
[GearType.RangedWeapons]: "crosshairs",
|
||||
[GearType.Explosives]: "bomb",
|
||||
[GearType.PowerUps]: "bolt",
|
||||
[GearType.NavigationEnhancers]: "compass",
|
||||
[GearType.MusicalInstruments]: "music",
|
||||
[GearType.SocialItems]: "share-nodes",
|
||||
[GearType.BuildingTools]: "screwdriver-wrench",
|
||||
[GearType.Transport]: "car-side"
|
||||
}
|
||||
|
||||
function getGearTypeName(type) {
|
||||
return names[type]
|
||||
}
|
||||
|
||||
function getGearTypeIcon(type) {
|
||||
return icons[type]
|
||||
}
|
||||
|
||||
function enableGearType(gearAttributes, gearType) {
|
||||
return gearAttributes | (1 << gearType)
|
||||
}
|
||||
|
||||
function disableGearType(gearAttributes, gearType) {
|
||||
return gearAttributes & ~(1 << gearType)
|
||||
}
|
||||
|
||||
function isGearTypeEnabled(gearAttributes, gearType) {
|
||||
return (gearAttributes & (1 << gearType)) !== 0
|
||||
}
|
||||
|
||||
export { GearType, getGearTypeName, getGearTypeIcon, enableGearType, disableGearType, isGearTypeEnabled }
|
||||
8
client/app/src/Enums/MessageType.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
Output: 0,
|
||||
Info: 1,
|
||||
Warning: 2,
|
||||
Error: 3,
|
||||
Sensitive: 4,
|
||||
Max: 5
|
||||
}
|
||||
170
client/app/src/Pages/Player/About.svelte
Normal file
@@ -0,0 +1,170 @@
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let { transport } = $props()
|
||||
|
||||
let platform = $state(null)
|
||||
let version = $state(null)
|
||||
let compiler = $state(null)
|
||||
let compileTimestamp = $state(null)
|
||||
let isUsingInstance = $state(null)
|
||||
let instanceName = $state(null)
|
||||
let instanceMotd = $state(null)
|
||||
let instanceUrl = $state(null)
|
||||
let instanceUpdateState = $state(null)
|
||||
let totalPlaytime = $state(null)
|
||||
|
||||
let totalItems = $state(0)
|
||||
let totalModels = $state(0)
|
||||
let totalLevels = $state(0)
|
||||
|
||||
const dependencies = [
|
||||
{ name: "assimp", license: "https://github.com/assimp/assimp/blob/master/LICENSE" },
|
||||
{ name: "BGFX", license: "https://github.com/bkaradzic/bgfx/blob/master/LICENSE" },
|
||||
{ name: "Boost", license: "https://www.boost.org/users/license.html" },
|
||||
{ name: "Bullet Physics SDK", license: "https://github.com/bulletphysics/bullet3/blob/master/LICENSE.txt" },
|
||||
{ name: "cURL", license: "https://curl.se/docs/copyright.html" },
|
||||
{ name: "CGAL", license: "https://www.cgal.org/license.html" },
|
||||
{ name: "Discord RPC", license: "https://github.com/discord/discord-rpc/blob/master/LICENSE" },
|
||||
{ name: "GLAD", license: "https://github.com/Dav1dde/glad/blob/glad2/LICENSE" },
|
||||
{ name: "libjpeg-turbo", license: "https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/LICENSE.md" },
|
||||
{ name: "FreeType", license: "https://freetype.org/license.html" },
|
||||
{ name: "ImGui", license: "https://www.dearimgui.com/licenses/" },
|
||||
{ name: "ImPlot", license: "https://github.com/epezent/implot/blob/master/LICENSE" },
|
||||
{ name: "libarchive", license: "https://raw.githubusercontent.com/libarchive/libarchive/master/COPYING" },
|
||||
{ name: "libjxl", license: "https://github.com/libjxl/libjxl/blob/main/LICENSE" },
|
||||
{ name: "libpng", license: "http://www.libpng.org/pub/png/src/libpng-LICENSE.txt" },
|
||||
{ name: "lz4", license: "https://github.com/lz4/lz4/blob/dev/LICENSE" },
|
||||
{ name: "microprofile", license: "https://github.com/jonasmr/microprofile/blob/master/LICENSE" },
|
||||
{ name: "OpenSSL", license: "https://www.openssl.org/source/apache-license-2.0.txt" },
|
||||
{ name: "opus", license: "https://opus-codec.org/license/" },
|
||||
{ name: "PortAudio", license: "https://files.portaudio.com/docs/v19-doxydocs/License.html" },
|
||||
{ name: "pugixml", license: "https://pugixml.org/license.html" },
|
||||
{ name: "Qt 6", license: "https://doc.qt.io/qt-6/lgpl.html" },
|
||||
{ name: "RakNet", license: "https://github.com/facebookarchive/RakNet/blob/master/LICENSE" },
|
||||
{ name: "RapidJSON", license: "https://github.com/Tencent/rapidjson/blob/master/license.txt" },
|
||||
{ name: "SDL3", license: "https://www.libsdl.org/license.php" },
|
||||
{ name: "xxHash", license: "https://github.com/Cyan4973/xxHash/blob/dev/LICENSE" },
|
||||
{ name: "zlib", license: "https://www.zlib.net/zlib_license.html" },
|
||||
{ name: "zstd", license: "https://github.com/facebook/zstd/blob/dev/LICENSE" }
|
||||
]
|
||||
|
||||
const cardClass = "mx-4 my-2 p-4 rounded rounded-lg border border-slate-100 bg-gray-50 shadow dark:bg-neutral-900 dark:border-neutral-800"
|
||||
const iconClass = "me-1 [image-rendering:pixelated]"
|
||||
const statClass = "flex items-center text-stone-800 dark:text-stone-300 font-medium"
|
||||
|
||||
function humanizeDuration(seconds) {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
return minutes > 0 ? `${hours}h ${minutes}m` : `${minutes}m ${seconds % 60}s`
|
||||
}
|
||||
|
||||
function humanizeTimestamp(timestamp) {
|
||||
const date = new Date(timestamp * 1000)
|
||||
|
||||
return `${date.toDateString()} ${date.toTimeString().split(" ")[0]}`
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!transport) return
|
||||
|
||||
[platform, version, isUsingInstance, totalPlaytime, compiler, compileTimestamp, instanceUpdateState] = await Promise.all([
|
||||
transport.getPlatformName(),
|
||||
transport.getVersion(),
|
||||
transport.isUsingInstance(),
|
||||
transport.getTotalPlaytime().then(humanizeDuration),
|
||||
transport.getCompilerName(),
|
||||
transport.getCompileTime().then(humanizeTimestamp),
|
||||
transport.getInstanceUpdateState()
|
||||
]); // this semicolon is very important... svelte's js parser breaks without it!
|
||||
|
||||
[totalLevels, totalItems, totalModels] = await Promise.all([
|
||||
transport.getTotalLevels(),
|
||||
transport.getTotalItems(),
|
||||
transport.getTotalModels()
|
||||
])
|
||||
|
||||
if (isUsingInstance) {
|
||||
[instanceUrl, instanceName, instanceMotd] = await Promise.all([
|
||||
transport.getInstanceURL(),
|
||||
transport.getInstanceName(),
|
||||
transport.getInstanceMotd()
|
||||
])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="{cardClass} flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<img alt="Aya" class="select-none pointer-events-none" src="./img/aya.png" width="80" />
|
||||
<div class="ms-5 flex flex-col">
|
||||
<span class="text-3xl mb-1.5 select-none"><b>Aya</b> for {platform ?? "Unknown"}</span>
|
||||
<code>{version ? `v${version}` : "N/A"}</code>
|
||||
<span class="text-sm"><span class="select-none">Compiled on</span><code class="ms-1">{compileTimestamp ?? "N/A"} ({compiler ?? "N/A"})</code></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center pointer-events-none select-none space-y-0.5 mx-1">
|
||||
<span class={statClass}><img alt="Levels" class={iconClass} src="./img/icons/level.png">{totalLevels ?? 0} levels</span>
|
||||
<span class={statClass}><img alt="Items" class={iconClass} src="./img/icons/item.png">{totalItems ?? 0} items</span>
|
||||
<span class={statClass}><img alt="Models" class={iconClass} src="./img/icons/model.png">{totalModels ?? 0} models</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isUsingInstance}
|
||||
<div class="{cardClass} mb-2 flex items-center">
|
||||
<img alt="{instanceName}" class="select-none pointer-events-none" src="./img/small.png" width="80" />
|
||||
<div class="ms-5 flex flex-col">
|
||||
<span class="mb-1 text-3xl select-none font-bold">{instanceName ?? "Unknown"}</span>
|
||||
<div class="mb-1 flex items-center">
|
||||
<span class="text-aya-500"><i class="fa-regular fa-globe me-1"></i><a href="{instanceUrl}" target="_blank" class="underline">{instanceUrl ?? "https://unknown.org/"}</a></span>
|
||||
<i class="fa-regular fa-pipe"></i>
|
||||
{#if instanceUpdateState && instanceUpdateState.enabled}
|
||||
<span class="text-green-600 dark:text-green-400 font-semibold flex items-center"><i class="fa-solid fa-cloud fa-fw me-1"></i>Updates enabled <span class="text-xs ms-1">(last updated: {humanizeTimestamp(instanceUpdateState.lastUpdated)})</span></span>
|
||||
{:else}
|
||||
<span class="text-gray-500 dark:text-gray-400 flex items-center"><i class="fa-regular fa-cloud-slash fa-fw me-1"></i>Updates disabled</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if instanceMotd}
|
||||
<span class="text-sm italic">“{instanceMotd}”</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center w-full select-none">
|
||||
<span class="text-gray-500 dark:text-gray-400 flex items-center mb-1"><i class="fa-regular fa-link-slash fa-fw me-1"></i>Not currently linked to an instance</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2 flex flex-col items-center justify-center w-full select-none">
|
||||
<span class="text-stone-600 dark:text-neutral-200 font-semibold flex items-center"><i class="fa-regular fa-clock fa-fw me-1"></i>{totalPlaytime ?? "0m 0s"} of total playtime</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 flex items-center w-full justify-center flex-col select-none">
|
||||
<span class="text-3xl font-extrabold">License</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-neutral-200 dark:border-neutral-700 dark:bg-neutral-800/80 m-1.5 mb-4 flex flex-col flex-1 min-h-0">
|
||||
<div class="p-4 font-mono text-[12.5px] overflow-y-auto">
|
||||
<span class="font-semibold">Due to the fact that Aya is a fork of previously leaked proprietary Roblox source code, it is not possible to apply a traditional license to Aya. Therefore, all authors of custom non-proprietary code and related resources used in Aya hereby irrevocably waive all copyright and related rights to their contributions, to the maximum extent allowed by law.</span>
|
||||
<br />
|
||||
<br />Please note that this waiver of copyright applies only to the non proprietary items found in Aya. Any proprietary code that may still exist in Aya remains subject to the copyright and licensing conditions imposed by its original authors or owners; in particular, Roblox Corporation.
|
||||
<br />
|
||||
<br />Aya uses a variety of third party dependencies. A list of all the third party dependencies used in Aya alongside a link to their respective license are available below:
|
||||
<br />
|
||||
{#each dependencies as dep}
|
||||
<br /><i class="fa-sharp fa-dot mx-1"></i>{dep.name}: <a class="text-aya-500 underline" target="_blank" href="{dep.license}">{dep.license}</a>
|
||||
{/each}
|
||||
<br />
|
||||
<br />The authors of Aya have made a concerted effort to rid the original codebase of any and all proprietary or otherwise closed source dependencies, and to replace them with free and open source alternatives. The only proprietary or non-free items that still remain in this repository is code and artwork created by Roblox Corporation, which themselves have undergone substantial modification to the extent that they no longer resemble their original versions.
|
||||
<br />
|
||||
<br /><span class="underline">It is the duty of anyone who uses Aya to be fully aware of the legal circumstances surrounding its use. The original authors of Aya expressly disclaim all liability for any and all uses of Aya, including, without limitation, any direct, indirect, incidental, special, consequential, or exemplary damages, even if advised of the possibility of such damages. The original authors of Aya further disclaim any and all responsibility for any third party's use or misuse of Aya.</span>
|
||||
<br />
|
||||
<br /><b>THE MATERIALS IN THIS REPOSITORY, INCLUDING ALL SOURCE CODE AND OTHER RELATED ITEMS, SUCH AS DOCUMENTATION, ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR ANY OTHER COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.</b>
|
||||
<br />
|
||||
<br /><span class="italic">In addition to the legal responsibilities outlined above, we strongly encourage all users of Aya to use this software in a responsible and ethical manner. Please respect the rights and dignity of others, and use Aya only in a way that contributes positively to the world.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
12
client/app/src/Pages/Player/Avatar.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import Button from "@/Controls/Button.svelte"
|
||||
import PillButtons from "@/Controls/PillButtons.svelte"
|
||||
|
||||
let { transport } = $props();
|
||||
|
||||
</script>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
3
client/app/src/Pages/Player/Favorites.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Favorites</h1>
|
||||
</div>
|
||||
7
client/app/src/Pages/Player/Levels.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let { transport } = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Browse Roblox Levels</h1>
|
||||
</div>
|
||||
7
client/app/src/Pages/Player/Packages.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let { transport } = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Manage Packages</h1>
|
||||
</div>
|
||||
120
client/app/src/Pages/Player/Play.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import ServerBrowser from "@/Components/ServerBrowser.svelte"
|
||||
import Button from "@/Controls/Button.svelte"
|
||||
import InputLabel from "@/Controls/InputLabel.svelte"
|
||||
import TextInput from "@/Controls/TextInput.svelte"
|
||||
import InputError from "@/Controls/InputError.svelte"
|
||||
import Card from "@/Components/Card.svelte"
|
||||
|
||||
let { transport, onNavigate } = $props();
|
||||
|
||||
let ipAddress = $state("")
|
||||
let port = $state("")
|
||||
let error = $state("")
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
const baseUrl = transport.getMasterServerURL()
|
||||
const userId = Math.floor(Math.random() * 65535)
|
||||
|
||||
let bodyColors = []
|
||||
let charApp = []
|
||||
|
||||
let charAppValue = transport.characterAppearanceJSON()
|
||||
|
||||
console.error("initial: " + charAppValue);
|
||||
|
||||
let selectedAssets = JSON.parse(charAppValue);
|
||||
|
||||
if (selectedAssets.hat) {
|
||||
selectedAssets.hat.forEach(function (assetId) {
|
||||
charApp.push(assetId);
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedAssets.normal) {
|
||||
for (const subCategory in selectedAssets.normal) {
|
||||
if (selectedAssets.normal.hasOwnProperty(subCategory)) {
|
||||
const assetIds = selectedAssets.normal[subCategory];
|
||||
assetIds.forEach(function (assetId) {
|
||||
charApp.push(assetId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error("charapp: " + JSON.stringify(charApp))
|
||||
|
||||
let bodyColorsValue = transport.getBodyColorJson()
|
||||
bodyColors = JSON.parse(bodyColorsValue);
|
||||
|
||||
const finalCharApp = baseUrl + '/avatar-fetch/?userid=' + userId + '&json=' + encodeURIComponent(JSON.stringify(charApp)) + '&body=' + encodeURIComponent(JSON.stringify(bodyColors));
|
||||
|
||||
const connectionObj = {
|
||||
ClientPort: 0,
|
||||
MachineAddress: ipAddress,
|
||||
ServerPort: Number(port),
|
||||
UserName: "Player",
|
||||
DisplayName: "Player",
|
||||
CharacterAppearance: finalCharApp,
|
||||
GameId: 1818,
|
||||
PlaceId: 1818,
|
||||
PingInterval: 20,
|
||||
UserId: userId,
|
||||
CreatorId: 1,
|
||||
MembershipType: "None",
|
||||
SuperSafeChat: false,
|
||||
IsUnknownOrUnder13: false,
|
||||
CreatorTypeEnum: "User",
|
||||
ChatStyle: "ClassicAndBubble",
|
||||
VirtualVersion: 0,
|
||||
IsRobloxPlace: true
|
||||
}
|
||||
|
||||
if (connectionObj.VirtualVersion === 4) {
|
||||
transport.enableChatBarWidget()
|
||||
}
|
||||
|
||||
await transport.launchGame(JSON.stringify(connectionObj), connectionObj.VirtualVersion)
|
||||
} catch (err) {
|
||||
error = "Failed to connect to server: " + err.message
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid h-full grid-rows-[auto,1fr] gap-3">
|
||||
<div class="col-span-1 grid grid-cols-2 gap-3">
|
||||
<Card title="Direct Connect" class="col-span-1" icon="fa-plug">
|
||||
<form onsubmit={preventDefault(submit)}>
|
||||
<div>
|
||||
<InputLabel for="ip" value="IP Address" />
|
||||
<TextInput placeholder="127.0.0.1" class="mt-1" id="ip" type="text" bind:value={ipAddress} required />
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<InputLabel for="port" value="Port" />
|
||||
<TextInput placeholder="53640" class="mt-1" id="port" type="number" bind:value={port} required />
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<InputError message={error} />
|
||||
{/if}
|
||||
|
||||
<span class="mt-2 block text-sm italic text-neutral-500 dark:text-neutral-300">
|
||||
You will be joining as <b>Player</b>.
|
||||
<button type="button" onclick={() => onNavigate('avatar')} class="underline">Customize Character</button>
|
||||
</span>
|
||||
|
||||
<div class="mt-2 flex w-full justify-end">
|
||||
<Button class="aya-btn-sm !bg-transparent !text-pink-400 !duration-0 hover:!bg-transparent hover:!text-pink-500" title="Add to Favorites" icon="fa-lg fa-heart" />
|
||||
<Button class="aya-btn-sm" type="submit" text="Join" icon="fa-right-to-bracket" title="Connect to Server" />
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="Play Solo" class="col-span-1" icon="fa-play">placeholder</Card>
|
||||
</div>
|
||||
|
||||
<ServerBrowser {transport} />
|
||||
</div>
|
||||
79
client/app/src/Pages/Player/Settings.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Button from "@/Controls/Button.svelte"
|
||||
import InputLabel from "@/Controls/InputLabel.svelte"
|
||||
import TextInput from "@/Controls/TextInput.svelte"
|
||||
import InputError from "@/Controls/InputError.svelte"
|
||||
import Card from "@/Components/Card.svelte"
|
||||
|
||||
let { transport } = $props();
|
||||
|
||||
let masterServerURL = $state("")
|
||||
let masterServerKey = $state("")
|
||||
let serverHostPassword = $state("")
|
||||
let robloSecurityCookie = $state("")
|
||||
let error = $state("")
|
||||
|
||||
onMount(() => {
|
||||
getSettings();
|
||||
});
|
||||
|
||||
async function getSettings() {
|
||||
masterServerURL = await transport.getMasterServerURL() ?? ""
|
||||
robloSecurityCookie = await transport.getRobloSecurityCookie() ?? ""
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
let a = '';
|
||||
(Object.getOwnPropertyNames(transport)).forEach(element => {
|
||||
a += (element) + "\n"
|
||||
});
|
||||
|
||||
try {
|
||||
transport.setMasterServerURL(masterServerURL)
|
||||
transport.setRobloSecurityCookie(robloSecurityCookie)
|
||||
} catch (err) {
|
||||
error = err + "\n" + a
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid h-full grid-rows-[auto,1fr] gap-3">
|
||||
<div class="col-span-1 grid grid-cols-3 gap-3">
|
||||
This page is temporary.
|
||||
<Card title="General Settings" class="col-span-1" icon="fa-gear">
|
||||
<form onsubmit={preventDefault(submit)}>
|
||||
<div>
|
||||
<InputLabel for="masterserverurl" value="Master Server URL" />
|
||||
<TextInput placeholder="http://masterserver.com/" class="mt-1" id="masterserverurl" type="text" bind:value={masterServerURL} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="masterserverkey" value="Master Server Key" />
|
||||
<TextInput placeholder="http://masterserver.com/" class="mt-1" id="masterserverkey" type="password" bind:value={masterServerKey} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="hostserverpassword" value="Host Server Password" />
|
||||
<TextInput placeholder="" class="mt-1" id="hostserverpassword" type="password" bind:value={serverHostPassword} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="securitycookie" value="ROBLOSECURITY Cookie" />
|
||||
<TextInput placeholder=".ROBLOSECURITY=_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|" class="mt-1" id="securitycookie" type="password" bind:value={robloSecurityCookie} />
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<InputError message={error} />
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 flex w-full justify-end">
|
||||
<Button class="aya-btn-sm" type="submit" text="Update Settings" icon="fa-gear" title="Update Settings" />
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
178
client/app/src/Pages/Server/Host.svelte
Normal file
@@ -0,0 +1,178 @@
|
||||
<script>
|
||||
import { ChatStyle, getChatStyleName } from "@/Enums/ChatStyle"
|
||||
import { GearType, getGearTypeName, getGearTypeIcon } from "@/Enums/GearType"
|
||||
|
||||
import Button from "@/Controls/Button.svelte"
|
||||
import TextInput from "@/Controls/TextInput.svelte"
|
||||
import Card from "@/Components/Card.svelte"
|
||||
|
||||
import Select from "@/Controls/Select.svelte"
|
||||
import InputLabel from "@/Controls/InputLabel.svelte"
|
||||
import Checkbox from "@/Controls/Checkbox.svelte"
|
||||
import TextArea from "@/Controls/TextArea.svelte"
|
||||
|
||||
let { transport } = $props();
|
||||
|
||||
let showServerSettings = $state(false)
|
||||
let isTransitioning = $state(false)
|
||||
let portValue = $state("")
|
||||
let fileName = $state(null)
|
||||
let serverPassword = $state(null)
|
||||
let serverName = $state("")
|
||||
let serverHost = $state("")
|
||||
let serverDescription = $state("")
|
||||
let chatStyle = $state(ChatStyle.ClassicAndBubble)
|
||||
let progress = $state(false)
|
||||
let error = $state(false)
|
||||
let broadcast = $state(false)
|
||||
let masterServerUrl = $state("")
|
||||
let masterServerKey = $state("")
|
||||
|
||||
let isPublicDomain = $state(false)
|
||||
|
||||
let maxPlayers = $state(16)
|
||||
|
||||
let enabledGearTypes = $state(Object.values(GearType).map(() => false))
|
||||
|
||||
let gearAttributes = $derived(enabledGearTypes.reduce((acc, isEnabled, index) => {
|
||||
return isEnabled ? acc | (1 << index) : acc
|
||||
}, 0))
|
||||
|
||||
function handleGearChange(index, isChecked) {
|
||||
enabledGearTypes[index] = isChecked
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
isTransitioning = true
|
||||
setTimeout(() => {
|
||||
showServerSettings = !showServerSettings
|
||||
isTransitioning = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function handleFileSelect(event) {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
fileName = file.name
|
||||
console.log("Selected file:", file.name)
|
||||
}
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
progress = true
|
||||
setTimeout(() => (progress = false), 5000)
|
||||
error = true
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showServerSettings}
|
||||
<div class="lex h-full w-full flex-col transition-opacity duration-300 ease-in-out" class:opacity-0={isTransitioning} class:opacity-100={!isTransitioning}>
|
||||
<div class="flex flex-shrink-0 items-center p-1">
|
||||
<button onclick={toggleSettings} class="aya-anim-pop flex h-10 items-center justify-center rounded px-3 font-medium text-neutral-500 transition duration-100 hover:bg-black/10 hover:text-neutral-600 dark:text-neutral-300 dark:hover:bg-white/10">
|
||||
<i class="fa fa-solid fa-fw fa-arrow-left me-2"></i>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 px-3 pb-24 pt-1">
|
||||
<Card class="flex flex-col">
|
||||
<div>
|
||||
<InputLabel for="max_players" value="Maximum amount of players" />
|
||||
<TextInput class="mt-2" id="max_players" type="number" bind:value={maxPlayers} required />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="server_password" value="Server password (optional)" />
|
||||
<TextInput class="mt-2" id="server_password" type="password" placeholder="Leave empty for no password" bind:value={serverPassword} />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="chat_style" value="Chat style" />
|
||||
<Select bind:value={chatStyle}>
|
||||
{#each Object.values(ChatStyle) as style}
|
||||
<option value={style} selected={chatStyle === style || null}>{getChatStyleName(style)}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<Checkbox name="is_public_domain" label="Allow connected players to download your place" bind:checked={isPublicDomain} />
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Checkbox name="broadcast" label="Broadcast server details to a masterserver" bind:checked={broadcast} />
|
||||
</div>
|
||||
|
||||
{#if broadcast}
|
||||
<div class="mt-2 select-none text-sm font-medium italic text-red-500 dark:text-red-400">Details about your server (IP address, port, connected players, etc.) will be publicly visible on the masterserver and to anyone who connects to it. Please make sure you trust the masterserver you are connecting to.</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="master_server_url" value="Masterserver base URL" />
|
||||
<TextInput class="mt-2" id="master_server_url" type="text" placeholder="e.g. masterserver.example.com" bind:value={masterServerUrl} required />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="master_server_key" value="Masterserver authentication key (optional)" />
|
||||
<TextInput class="mt-2" id="master_server_key" type="text" bind:value={masterServerKey} />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="server_name" value="Server Name" />
|
||||
<TextInput class="mt-2" id="server_name" placeholder="Max 50 characters" type="text" bind:value={serverName} required />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="server_host" value="Server Host" />
|
||||
<TextInput class="mt-2" id="server_host" placeholder="Typically your username" type="text" bind:value={serverHost} required />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="server_description" value="Server Description" />
|
||||
<TextArea class="mt-2" id="server_description" placeholder="Max 1500 characters" bind:value={serverDescription} required />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-3">
|
||||
<InputLabel for="name" value="Allowed gears" />
|
||||
<div class="mt-1 grid grid-cols-2 gap-1 md:grid-cols-3">
|
||||
{#each Object.entries(GearType) as [typeName, typeValue], index}
|
||||
<Checkbox name={getGearTypeName(typeValue).toString().toLowerCase().replace(" ", "_")} bind:checked={enabledGearTypes[index]} onchange={() => handleGearChange(index, enabledGearTypes[index])}>
|
||||
{getGearTypeName(typeValue)}
|
||||
<i class="fa-regular fa-fw fa-{getGearTypeIcon(typeValue)} ms-1"></i>
|
||||
</Checkbox>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Main Page -->
|
||||
<div class="flex h-full w-full flex-col items-center justify-center transition-opacity duration-300 ease-in-out" class:opacity-0={isTransitioning} class:opacity-100={!isTransitioning}>
|
||||
<img src="./img/aya-server{progress ? '-progress' : error ? '-error' : ''}.png" width="150" alt="Aya Server" class="pointer-events-none select-none" />
|
||||
|
||||
<span class="my-5 select-none text-4xl font-extrabold text-stone-900 dark:text-stone-50">Aya Server</span>
|
||||
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<div class="flex w-full max-w-64 flex-col">
|
||||
<label for="place-file" class="aya-anim-pop flex cursor-pointer items-center rounded rounded-b-none border border-neutral-500/20 bg-neutral-500/5 px-3 py-1 text-lg text-neutral-500/85 transition duration-100 hover:bg-neutral-500/10">
|
||||
<div class="select-none">
|
||||
<i class="fa-light fa-folder-open fa-sm fa-fw me-1"></i>
|
||||
{fileName ?? "Select place file …"}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<input id="place-file" type="file" accept=".rbxl,.ayal,.rbxl.gz,.ayal.gz" class="hidden" onchange={handleFileSelect} />
|
||||
|
||||
<TextInput bind:value={portValue} min="0" max="65535" type="number" placeholder="Port (e.g. 53640)" class="!h-auto !rounded-t-none !border-t-0 py-1 text-lg" />
|
||||
</div>
|
||||
|
||||
<button class="aya-anim-pop mt-2 flex h-auto items-center justify-center rounded px-2 py-1 text-center text-neutral-500 transition duration-100 hover:bg-black/10 hover:text-neutral-600 dark:text-neutral-300 dark:hover:bg-white/10" onclick={toggleSettings}>
|
||||
<i class="fa-regular fa-square-sliders me-0.5"></i>
|
||||
Server Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button onclick={startServer} disabled={progress} icon="fa-play" class="mt-4 !w-auto !min-w-max !max-w-full !gap-1 !rounded-md !border !border-green-600 !bg-green-500 !shadow-lg dark:!border-green-700 dark:!bg-green-600 {progress ? 'pointer-events-none cursor-default opacity-50' : ''}" text="Start" />
|
||||
</div>
|
||||
{/if}
|
||||
57
client/app/src/Pages/Server/Jobs.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
let { transport } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col justify-start gap-3 overflow-scroll p-4 pb-24">
|
||||
<div class="flex w-full items-center"><span class="text-3xl font-extrabold">Running Jobs</span><button class="ml-auto flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100"><i class="fa fa-regular fa-play fa-fw me-2"></i>Start</button></div>
|
||||
|
||||
<div class="flex items-center rounded border border-neutral-400/75 bg-gray-50/90 px-3 py-4">
|
||||
<i class="fa fa-light fa-server fa-fw me-1 text-6xl text-neutral-600"></i>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xl font-bold text-neutral-900">127.0.0.1:53640</span>
|
||||
<span class="font-mono text-xs text-neutral-500">9f4e11e2-f13b-470f-b6cd-ada7d82cdf2f</span>
|
||||
</div>
|
||||
<!-- these go on the far right -->
|
||||
<div class="ml-auto flex gap-3">
|
||||
<button class="flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100">
|
||||
<i class="fa fa-regular fa-eye fa-fw me-2"></i>Inspect
|
||||
</button>
|
||||
<button class="flex items-center rounded border border-red-400 bg-red-500/10 px-3 py-1 text-sm text-red-600 shadow-sm hover:bg-red-500/20">
|
||||
<i class="fa fa-regular fa-xmark fa-fw me-2"></i>Stop Job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center rounded border border-neutral-400/75 bg-gray-50/90 px-3 py-4">
|
||||
<i class="fa fa-light fa-server fa-fw me-1 text-6xl text-neutral-600"></i>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xl font-bold text-neutral-900">Test Job</span>
|
||||
<span class="font-mono text-sm text-neutral-500">a27df45e-670e-4a6b-94fa-d65f9ce844f3</span>
|
||||
</div>
|
||||
<!-- these go on the far right -->
|
||||
<div class="ml-auto flex gap-3">
|
||||
<button class="flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100">
|
||||
<i class="fa fa-regular fa-eye fa-fw me-2"></i>Inspect
|
||||
</button>
|
||||
<button class="flex items-center rounded border border-red-400 bg-red-500/10 px-3 py-1 text-sm text-red-600 shadow-sm hover:bg-red-500/20">
|
||||
<i class="fa fa-regular fa-xmark fa-fw me-2"></i>Stop Job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center rounded border border-neutral-400/75 bg-gray-50/90 px-3 py-4">
|
||||
<i class="fa fa-light fa-server fa-fw me-1 text-6xl text-neutral-600"></i>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xl font-bold text-neutral-900">yrdts</span>
|
||||
<span class="font-mono text-sm text-neutral-500">72110ea2-de5e-4c82-b203-05b62cac9aa7</span>
|
||||
</div>
|
||||
<!-- these go on the far right -->
|
||||
<div class="ml-auto flex gap-3">
|
||||
<button class="flex items-center rounded border border-neutral-300 bg-white px-3 py-1 text-sm text-neutral-600 shadow-sm hover:bg-gray-100">
|
||||
<i class="fa fa-regular fa-eye fa-fw me-2"></i>Inspect
|
||||
</button>
|
||||
<button class="flex items-center rounded border border-red-400 bg-red-500/10 px-3 py-1 text-sm text-red-600 shadow-sm hover:bg-red-500/20">
|
||||
<i class="fa fa-regular fa-xmark fa-fw me-2"></i>Stop Job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
7
client/app/src/Pages/Server/REPL.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
let { transport } = $props();
|
||||
</script>
|
||||
|
||||
<div class="h-screen px-4 pb-[4.75rem] pt-3">
|
||||
<div class="h-full w-full rounded bg-black outline outline-2 outline-neutral-600 dark:outline-white">HELLO</div>
|
||||
</div>
|
||||
40
client/app/src/Pages/Studio/IDE.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script>
|
||||
|
||||
import { onMount } from "svelte"
|
||||
let { transport } = $props();
|
||||
|
||||
let files = []
|
||||
|
||||
onMount(() => {
|
||||
let parameters = new URLSearchParams(window.location.search)
|
||||
let entries = Array.from(parameters.entries())
|
||||
|
||||
if (entries.length % 2 !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i += 2) {
|
||||
let filepath = entries[i][1]
|
||||
let filename = entries[i + 1][1]
|
||||
|
||||
files.push({ path: filepath, name: filename })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="p-5">
|
||||
<div class="mb-3 flex items-center">
|
||||
<img src="./img/aya-studio.png" width="75" />
|
||||
<div class="ms-3 flex flex-col">
|
||||
<span class="text-2xl font-extrabold">Aya Studio</span>
|
||||
<code>v1.0.0</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="mb-2 text-lg font-bold">Recent Files:</span>
|
||||
<ul>
|
||||
{#each files as file}
|
||||
<li><a href={`#${file.path}`}>{file.name}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
0
client/app/src/Pages/Studio/Toolbox.svelte
Normal file
77
client/app/src/Player.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script>
|
||||
import { spring } from "svelte/motion"
|
||||
|
||||
import PageButton from "@/Components/PlayerPageButton.svelte"
|
||||
import PlayPage from "@/Pages/Player/Play.svelte"
|
||||
import AvatarPage from "@/Pages/Player/Avatar.svelte"
|
||||
import LevelsPage from "@/Pages/Player/Levels.svelte"
|
||||
import PackagesPage from "@/Pages/Player/Packages.svelte"
|
||||
import FavoritesPage from "@/Pages/Player/Favorites.svelte"
|
||||
import SettingsPage from "@/Pages/Player/Settings.svelte"
|
||||
import AboutPage from "@/Pages/Player/About.svelte"
|
||||
|
||||
let { transport } = $props();
|
||||
|
||||
let selectedPage = $state("play")
|
||||
|
||||
const pages = [
|
||||
{ id: "play", title: "Play" },
|
||||
{ id: "avatar", title: "Character" },
|
||||
{ id: "levels", title: "Levels" },
|
||||
{ id: "packages", title: "Packages" },
|
||||
{ id: "favorites", title: "Favorites" },
|
||||
{ id: "server", title: "Open Aya Server" },
|
||||
{ id: "studio", title: "Open Aya Studio" },
|
||||
{ id: "settings", title: "Settings", icon: "fa-cog" },
|
||||
{ id: "about", title: "About", icon: "fa-circle-info" }
|
||||
]
|
||||
|
||||
let pageIndicator = spring(9.5, {
|
||||
stiffness: 0.15,
|
||||
damping: 0.7
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (selectedPage) {
|
||||
const activeButton = document.querySelector(`[data-page="${selectedPage}"]`)
|
||||
if (activeButton) {
|
||||
pageIndicator.set(activeButton.offsetTop + 2)
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen">
|
||||
<div class="flex w-[3.5rem] flex-col items-center border-r border-gray-300 bg-gray-100 pb-0.5 pt-2 text-white dark:border-neutral-950 dark:bg-neutral-900">
|
||||
<div class="absolute left-0 h-6 w-[0.175rem] rounded-r bg-aya-500" style="transform: translateY({$pageIndicator}px); transition: height 0.2s ease" role="presentation"></div>
|
||||
{#each pages as page}
|
||||
{#if page.id == "server"}
|
||||
<div class="flex-grow"></div>
|
||||
<PageButton {page} {selectedPage} onClick={() => (transport.launchStudio())} />
|
||||
{:else if page.id == "studio"}
|
||||
<PageButton {page} {selectedPage} onClick={() => (transport.launchStudio())} />
|
||||
{:else}
|
||||
<PageButton {page} {selectedPage} onClick={() => (selectedPage = page.id)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<main class="flex-1 p-3 dark:bg-stone-950 dark:text-neutral-100 {selectedPage === 'avatar' ? 'border-r border-gray-300' : ''}">
|
||||
{#if selectedPage === "play"}
|
||||
<PlayPage {transport} onNavigate={(page) => selectedPage = page} />
|
||||
{:else if selectedPage === "avatar"}
|
||||
<AvatarPage {transport} />
|
||||
{:else if selectedPage === "levels"}
|
||||
<LevelsPage {transport} />
|
||||
{:else if selectedPage === "packages"}
|
||||
<PackagesPage {transport} />
|
||||
{:else if selectedPage === "favorites"}
|
||||
<FavoritesPage />
|
||||
{:else if selectedPage === "settings"}
|
||||
<SettingsPage {transport} />
|
||||
{:else if selectedPage === "about"}
|
||||
<AboutPage {transport} />
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
46
client/app/src/Server.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script>
|
||||
|
||||
import PageButton from "@/Components/ServerPageButton.svelte"
|
||||
import HostPage from "@/Pages/Server/Host.svelte"
|
||||
import JobPage from "@/Pages/Server/Jobs.svelte"
|
||||
import REPLPage from "@/Pages/Server/REPL.svelte"
|
||||
let { transport } = $props();
|
||||
|
||||
let selectedPage = $state("host")
|
||||
let isTransitioning = $state(false)
|
||||
|
||||
const pages = [
|
||||
{ icon: "fa-tower-broadcast", id: "host", title: "Host" },
|
||||
{ icon: "fa-server", id: "jobs", title: "Jobs" },
|
||||
{ icon: "fa-rectangle-terminal", id: "repl", title: "REPL" }
|
||||
]
|
||||
|
||||
function changePage(newPage) {
|
||||
if (newPage === selectedPage) return
|
||||
|
||||
isTransitioning = true
|
||||
setTimeout(() => {
|
||||
selectedPage = newPage
|
||||
isTransitioning = false
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen w-screen flex-col items-center justify-center dark:bg-stone-950 dark:text-neutral-100">
|
||||
<div class=" contents-center absolute bottom-0 left-0 z-50 flex w-full items-center justify-center gap-2 border-t border-gray-300 bg-gray-100 py-1.5 dark:border-neutral-950 dark:bg-neutral-900">
|
||||
{#each pages as page}
|
||||
<PageButton {page} {selectedPage} onClick={() => changePage(page.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
<main class="h-full w-full overflow-scroll">
|
||||
<div class="h-full w-full transition-opacity duration-300 {isTransitioning ? 'opacity-0' : 'opacity-100'}">
|
||||
{#if selectedPage === "host"}
|
||||
<HostPage {transport} />
|
||||
{:else if selectedPage === "jobs"}
|
||||
<JobPage {transport} />
|
||||
{:else if selectedPage === "repl"}
|
||||
<REPLPage {transport} />
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
15
client/app/src/Studio.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
|
||||
import IDEPage from "@/Pages/Studio/IDE.svelte"
|
||||
import ToolboxPage from "@/Pages/Studio/Toolbox.svelte"
|
||||
let { transport } = $props();
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const page = params.get("page") || "ide" // can be of [ide, toolbox] to access two different UIs
|
||||
</script>
|
||||
|
||||
{#if page === "ide"}
|
||||
<IDEPage {transport} />
|
||||
{:else if page === "toolbox"}
|
||||
<ToolboxPage {transport} />
|
||||
{/if}
|
||||