16 Commits

Author SHA1 Message Date
Wanjohi
8aa983834c Merge branch 'main' into feat/play 2025-03-16 00:11:54 +03:00
AquaWolf
5189bf768a merge 2025-03-15 21:06:30 +01:00
AquaWolf
b734892c55 extracted modal component + showing modal on enter and mouspointer loss 2025-03-09 14:53:11 +01:00
AquaWolf
402e894224 added modal with correct styling. Open: need to wire the modals correctly and have the welcome modal 2025-03-03 22:28:53 +01:00
Wanjohi
fb0cb0b6ca fix: Portal mount 2025-03-03 23:47:00 +03:00
Wanjohi
4fd339b55f fix: Have a root component 2025-03-03 23:42:58 +03:00
Wanjohi
1e78238593 fix: Colors 2025-03-03 23:36:18 +03:00
AquaWolf
c994dc112c added new App changes and background for theme 2025-03-03 21:16:27 +01:00
AquaWolf
a727a9b710 some wip with styles 2025-03-03 21:07:13 +01:00
AquaWolf
805a8a6115 some changes to the play route 2025-03-03 17:39:38 +01:00
Wanjohi
05aa177681 Merge branch 'main' into feat/play 2025-03-03 15:00:31 +03:00
Wanjohi
421fcb067c Merge branch 'main' into feat/play 2025-03-02 14:58:11 +03:00
Wanjohi
7dee7e480b Merge branch 'main' into feat/play 2025-03-02 01:31:37 +03:00
Wanjohi
1a49c709f7 Merge branch 'main' into feat/play 2025-03-02 00:11:22 +03:00
AquaWolf
90e0533fdd right parameter 2025-02-28 21:47:46 +01:00
AquaWolf
058ac24954 added first draft of the play route 2025-02-28 21:30:23 +01:00
7 changed files with 440 additions and 3 deletions

View File

@@ -30,6 +30,7 @@
"@solidjs/router": "^0.15.3",
"modern-normalize": "^3.0.1",
"solid-js": "^1.9.5",
"solid-notifications": "^1.1.2",
"valibot": "^1.0.0-rc.3",
"zod": "^3.24.2"
}

View File

@@ -6,6 +6,7 @@ import '@fontsource/geist-sans/600.css';
import '@fontsource/geist-sans/700.css';
import '@fontsource/geist-sans/800.css';
import '@fontsource/geist-sans/900.css';
import { PlayComponent } from './pages/play';
import { styled } from "@macaron-css/solid";
import { useStorage } from './providers/account';
import { CreateTeamComponent } from './pages/new';
@@ -14,6 +15,7 @@ import { AuthProvider, useAuth } from './providers/auth';
import { Navigate, Route, Router } from "@solidjs/router";
import { globalStyle, macaron$ } from "@macaron-css/core";
import { Component, createSignal, Match, onCleanup, Switch } from 'solid-js';
import TestComponent from './pages/test';
const Root = styled("div", {
base: {
@@ -93,6 +95,7 @@ export const App: Component = () => {
// props.children
)}
>
<Route path="play/:id" component={PlayComponent} />
<Route path="new" component={CreateTeamComponent} />
<Route
path="/"

View File

@@ -0,0 +1,80 @@
import { Component, JSX, Show, createSignal } from "solid-js";
import { Portal } from "solid-js/web";
import { styled } from "@macaron-css/solid";
import { theme } from "@nestri/www/ui/theme";
const ModalContainer = styled("div", {
base: {
width: "100%",
maxWidth: 370,
maxHeight: "75vh",
height: "auto",
borderRadius: 12,
borderWidth: 1,
borderStyle: "solid",
borderColor: theme.color.gray.d400,
backgroundColor: theme.color.gray.d200,
boxShadow: theme.color.boxShadow,
backdropFilter: "blur(20px)",
padding: "20px 25px"
}
})
export interface ModalProps {
isOpen?: boolean;
onClose?: ((value: boolean) => void) | (() => void);
children: JSX.Element;
mountPoint?: HTMLElement;
containerClass?: string;
overlayClass?: string;
}
export function createModalController() {
const [isOpen, setIsOpen] = createSignal(false);
return {
isOpen,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen(!isOpen()),
};
}
export const Modal: Component<ModalProps> = (props) => {
const mountPoint = props.mountPoint || document.getElementById("styled") || document.body;
const isOpen = () => props.isOpen ?? false;
const defaultOverlayStyle = `
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
z-index: 50;
`;
return (
<Portal mount={mountPoint}>
<Show when={isOpen()}>
<div
class={props.overlayClass}
style={!props.overlayClass ? defaultOverlayStyle : undefined}
onClick={(e) => {
if (e.target === e.currentTarget && props.onClose) {
if (props.onClose.length > 0) {
(props.onClose as (value: boolean) => void)(false);
} else {
(props.onClose as () => void)();
}
}
}}
>
<ModalContainer>
{props.children}
</ModalContainer>
</div>
</Show>
</Portal>
);
};

View File

@@ -8,6 +8,7 @@ import { render } from "solid-js/web";
import "modern-normalize/modern-normalize.css";
import { App } from "./App";
import { StorageProvider } from "./providers/account";
// import { ToastProvider, Toaster } from "solid-notifications";
const root = document.getElementById("root");
@@ -19,9 +20,12 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
render(
() => (
<StorageProvider>
<App />
</StorageProvider>
// <ToastProvider>
// <Toaster />
<StorageProvider>
<App />
</StorageProvider>
// </ToastProvider>
),
root!
);

View File

@@ -0,0 +1,326 @@
// FIXME: We need to make from the modal a reusable component
// FIXME: The mousepointer lock is somehow shifted when the window gets resized
import { Text } from "@nestri/www/ui/text";
import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js";
import { Portal } from "solid-js/web";
import { useParams } from "@solidjs/router";
import { Keyboard, Mouse, WebRTCStream } from "@nestri/input";
import { Container, FullScreen } from "@nestri/www/ui/layout";
import { styled } from "@macaron-css/solid";
import { lightClass, theme, darkClass } from "@nestri/www/ui/theme";
import { Modal, createModalController } from "../components/Modal";
const Canvas = styled("canvas", {
base: {
aspectRatio: 16 / 9,
width: "100%",
height: "100%",
objectFit: "contain",
maxHeight: "100vh",
}
});
const Button = styled("button", {
base: {
outline: "none",
width: "100%",
backgroundColor: theme.color.background.d100,
padding: "12px 16px",
borderRadius: 10,
borderWidth: 1,
borderStyle: "solid",
borderColor: theme.color.gray.d500,
":hover": {
backgroundColor: theme.color.gray.d300,
}
}
})
export function PlayComponent() {
const params = useParams();
const id = params.id;
const [showBannerModal, setShowBannerModal] = createSignal(false);
const [showButtonModal, setShowButtonModal] = createSignal(false);
const [gamepadConnected, setGamepadConnected] = createSignal(false);
const [buttonPressed, setButtonPressed] = createSignal(null);
const [leftStickX, setLeftStickX] = createSignal(0);
const [leftStickY, setLeftStickY] = createSignal(0);
const [hasStream, setHasStream] = createSignal(false);
const [nestriLock, setNestriLock] = createSignal(false);
const [showOffline, setShowOffline] = createSignal(false);
const [canvas, setCanvas] = createSignal<HTMLCanvasElement | undefined>(undefined);
let video: HTMLVideoElement;
let webrtc: WebRTCStream;
let nestriMouse: Mouse, nestriKeyboard: Keyboard;
const { Modal, openModal } = createModal();
const { WelcomeModal, openWelcomeModal } = createWelcomeModal();
const initializeInputDevices = () => {
const canvasElement = canvas();
if (!canvasElement || !webrtc) return;
try {
nestriMouse = new Mouse({ canvas: canvasElement, webrtc });
nestriKeyboard = new Keyboard({ canvas: canvasElement, webrtc });
console.log("Input devices initialized successfully");
} catch (error) {
console.error("Failed to initialize input devices:", error);
}
};
/*const initializeGamepad = () => {
console.log("Initializing gamepad...");
const updateGamepadState = () => {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[0];
if (gamepad) {
setButtonPressed(gamepad.buttons.findIndex(btn => btn.pressed) !== -1 ? "Button pressed" : null);
setLeftStickX(Number(gamepad.axes[0].toFixed(2)));
setLeftStickY(Number(gamepad.axes[1].toFixed(2)));
}
requestAnimationFrame(updateGamepadState);
};
window.addEventListener("gamepadconnected", () => {
setGamepadConnected(true);
console.log("Gamepad connected!");
updateGamepadState();
});
window.addEventListener("gamepaddisconnected", () => {
setGamepadConnected(false);
console.log("Gamepad disconnected!");
});
};*/
const lockPlay = async () => {
const canvasElement = canvas();
if (!canvasElement || !hasStream()) return;
try {
await canvasElement.requestPointerLock();
await canvasElement.requestFullscreen();
//initializeGamepad();
if (document.fullscreenElement !== null) {
if ('keyboard' in navigator && 'lock' in (navigator.keyboard as any)) {
const keys = [
"AltLeft", "AltRight", "Tab", "Escape",
"ContextMenu", "MetaLeft", "MetaRight"
];
try {
await (navigator.keyboard as any).lock(keys);
setNestriLock(true);
console.log("Keyboard lock acquired");
} catch (e) {
console.warn("Keyboard lock failed:", e);
setNestriLock(false);
}
}
}
} catch (error) {
console.error("Error during lock sequence:", error);
}
};
const setupPointerLockListener = () => {
document.addEventListener("pointerlockchange", () => {
const canvasElement = canvas();
if (!canvasElement) return;
if (document.pointerLockElement === canvasElement) {
initializeInputDevices();
} else {
console.log("Pointer lock lost Show Banner Modal:", showBannerModal());
if (!showBannerModal()) {
console.log("Pointer lock lost, showing banner");
const playing = sessionStorage.getItem("showedBanner");
setShowBannerModal(!playing || playing !== "true");
openWelcomeModal();
if (playing) {
setShowButtonModal(true);
openModal();
}
}
nestriKeyboard?.dispose();
nestriMouse?.dispose();
}
});
};
const handleVideoInput = async () => {
const canvasElement = canvas();
if (!video || !canvasElement) return;
try {
await video.play();
if (canvasElement && video) {
canvasElement.width = video.videoWidth;
canvasElement.height = video.videoHeight;
const ctx = canvasElement.getContext("2d");
const renderer = () => {
if (ctx && hasStream() && video) {
ctx.drawImage(video, 0, 0);
video.requestVideoFrameCallback(renderer);
}
};
video.requestVideoFrameCallback(renderer);
}
} catch (error) {
console.error("Error playing video:", error);
}
};
onMount(() => {
const canvasElement = canvas();
if (!canvasElement) return;
setupPointerLockListener();
video = document.createElement("video");
video.style.visibility = "hidden";
webrtc = new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => {
if (video && mediaStream) {
video.srcObject = mediaStream;
setHasStream(true);
setShowOffline(false);
const playing = sessionStorage.getItem("showedBanner")
console.log("Playing:", playing);
if (!playing || playing != "true") {
console.log("Showing banner: ", showBannerModal());
if (!showBannerModal()) {
setShowBannerModal(false)
openWelcomeModal();
}
} else {
if (!showButtonModal()) {
setShowButtonModal(true)
openModal();
}
}
await handleVideoInput();
} else if (mediaStream === null) {
console.log("MediaStream is null, Room is offline");
setShowOffline(true);
setHasStream(false);
const ctx = canvasElement.getContext("2d");
if (ctx) ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
} else if (video && video.srcObject !== null) {
setHasStream(true);
setShowOffline(true);
await handleVideoInput();
}
});
});
onCleanup(() => {
nestriKeyboard?.dispose();
nestriMouse?.dispose();
});
return (<FullScreen>
{showOffline() ? (
<div class="w-screen h-screen flex justify-center items-center">
<span class="text-2xl font-semibold flex items-center gap-2">Offline</span>
</div>
) : (
<Canvas ref={setCanvas} onClick={lockPlay} />
)}
<WelcomeModal
show={showBannerModal}
setShow={setShowBannerModal}
closeOnBackdropClick={false}
handleVideoInput={handleVideoInput}
lockPlay={lockPlay} />
<Modal show={showButtonModal}
setShow={setShowButtonModal}
closeOnBackdropClick={false}
handleVideoInput={handleVideoInput}
lockPlay={lockPlay} />
</FullScreen>
);
}
interface ModalProps {
show: () => boolean;
setShow: (value: boolean) => void;
closeOnBackdropClick?: boolean;
handleVideoInput?: () => Promise<void>;
lockPlay?: () => Promise<void>;
}
type GameModalProps = ModalProps & {
handleVideoInput?: () => Promise<void>;
lockPlay?: () => Promise<void>;
setShow?: (show: boolean) => void;
}
function createWelcomeModal() {
const controller = createModalController();
return {
openWelcomeModal: controller.open,
WelcomeModal(props: GameModalProps) {
return (
<Modal
isOpen={controller.isOpen()}
onClose={controller.close}
>
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
Happy that you use Nestri!
<Button onClick={async () => {
sessionStorage.setItem("showedBanner", "true");
await props.handleVideoInput?.();
await props.lockPlay?.();
controller.close();
}}>Let's go</Button>
</div>
</Modal>
);
},
};
}
function createModal() {
const controller = createModalController();
return {
openModal: controller.open,
Modal(props: GameModalProps) {
return (
<Modal
isOpen={controller.isOpen()}
onClose={controller.close}
>
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
<Button onClick={async () => {
props.setShow?.(false);
await props.handleVideoInput?.();
await props.lockPlay?.();
controller.close();
}}>Continue Playing</Button>
<Button onClick={controller.close}>Shutdown Nestri</Button>
</div>
</Modal>
);
},
};
}

View File

@@ -0,0 +1,18 @@
import { styled } from "@macaron-css/solid";
import { theme } from "../ui/theme";
const Testing = styled("div", {
base: {
height: "100%",
width: "100%",
position: "fixed",
backgroundColor: theme.color.blue.d600
}
})
export default function TestComponent() {
return (
<Testing />
)
}

View File

@@ -241,6 +241,7 @@ const light = (() => {
},
};
const boxShadow = "0 0 0 1px rgba(19,21,23,0.08), 0 3.3px 2.7px rgba(0,0,0,.03),0 8.3px 6.9px rgba(0,0,0,.04),0 17px 14.2px rgba(0,0,0,.05),0 35px 29.2px rgba(0,0,0,.06),0px -4px 4px 0px rgba(0,0,0,.07) inset";
return {
gray,
blue,
@@ -256,6 +257,7 @@ const light = (() => {
focusBorder,
focusColor,
d1000,
boxShadow,
brand,
text
};
@@ -403,6 +405,8 @@ const dark = (() => {
},
};
const boxShadow = "0 0 0 1px rgba(255,255,255,0.08), 0 3.3px 2.7px rgba(0,0,0,.1),0 8.3px 6.9px rgba(0,0,0,.13),0 17px 14.2px rgba(0,0,0,.17),0 35px 29.2px rgba(0,0,0,.22),0px -4px 4px 0px rgba(0,0,0,.04) inset";
return {
gray,
blue,
@@ -419,6 +423,7 @@ const dark = (() => {
focusColor,
d1000,
text,
boxShadow,
brand
};
})()