From 058ac24954e4f2aace79450eece1b494f7a74f97 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Fri, 28 Feb 2025 21:30:23 +0100 Subject: [PATCH 01/11] added first draft of the play route --- packages/www/package.json | 3 +- packages/www/src/App.tsx | 2 + packages/www/src/index.tsx | 10 ++- packages/www/src/pages/play.tsx | 149 ++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 packages/www/src/pages/play.tsx diff --git a/packages/www/package.json b/packages/www/package.json index 96c96968..fe20260c 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -27,6 +27,7 @@ "@solid-primitives/storage": "^4.3.1", "@solidjs/router": "^0.15.3", "modern-normalize": "^3.0.1", - "solid-js": "^1.9.5" + "solid-js": "^1.9.5", + "solid-notifications": "^1.1.2" } } \ No newline at end of file diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index efd3487d..59e032ad 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -7,6 +7,7 @@ import '@fontsource/geist-sans/700.css'; import '@fontsource/geist-sans/800.css'; import '@fontsource/geist-sans/900.css'; import { TeamCreate } from './pages/new'; +import { PlayComponent } from './pages/play'; import { styled } from "@macaron-css/solid"; import { useStorage } from './providers/account'; import { darkClass, lightClass, theme } from './ui/theme'; @@ -116,6 +117,7 @@ export const App: Component = () => { {WorkspaceRoute} */} + { diff --git a/packages/www/src/index.tsx b/packages/www/src/index.tsx index 608b9165..0bada0f9 100644 --- a/packages/www/src/index.tsx +++ b/packages/www/src/index.tsx @@ -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( () => ( - - - + + + + + + ), root! ); \ No newline at end of file diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx new file mode 100644 index 00000000..669f97fe --- /dev/null +++ b/packages/www/src/pages/play.tsx @@ -0,0 +1,149 @@ +import { Text } from "@nestri/www/ui/text"; +import { createSignal, createEffect, onCleanup, onMount } from "solid-js"; +import { useParams } from "@solidjs/router"; +import { Modal } from "@nestri/ui"; +import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; + +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 [showOffline, setShowOffline] = createSignal(false); + + const [canvas, setCanvas] = createSignal(undefined); + let video: HTMLVideoElement; + let webrtc: WebRTCStream; + let nestriMouse: Mouse , nestriKeyboard: Keyboard; + + 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(); + } 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 { + 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(() => { + 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); + await handleVideoInput(); + } else { + setShowOffline(true); + setHasStream(false); + } + }); + }); + + onCleanup(() => { + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + }); + + return ( + <> + {showOffline() ? ( +
+ Offline +
+ ) : ( + + )} + + ); + } \ No newline at end of file From 90e0533fdd384c5ba4392cc5f72de99a915a1eb6 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Fri, 28 Feb 2025 21:47:46 +0100 Subject: [PATCH 02/11] right parameter --- packages/www/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 59e032ad..965e7da9 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -117,7 +117,7 @@ export const App: Component = () => { {WorkspaceRoute} */} - + { From 805a8a611571c3bb7dfea9836f6d9b4bfd29d654 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 17:39:38 +0100 Subject: [PATCH 03/11] some changes to the play route --- packages/www/src/pages/play.tsx | 106 ++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 669f97fe..8c9dbb25 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,8 +1,8 @@ import { Text } from "@nestri/www/ui/text"; -import { createSignal, createEffect, onCleanup, onMount } from "solid-js"; +import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"; import { useParams } from "@solidjs/router"; -import { Modal } from "@nestri/ui"; import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"; +import { Container, FullScreen } from "@nestri/www/ui/layout"; export function PlayComponent() { const params = useParams(); @@ -15,6 +15,7 @@ export function PlayComponent() { 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(undefined); @@ -67,6 +68,25 @@ export function PlayComponent() { 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); } @@ -79,6 +99,15 @@ export function PlayComponent() { if (document.pointerLockElement === canvasElement) { initializeInputDevices(); } else { + + if (!showBannerModal) { + const playing = sessionStorage.getItem("showedBanner"); + setShowBannerModal(!playing || playing !== "true"); + setShowButtonModal(playing === "false"); + } + + + nestriKeyboard?.dispose(); nestriMouse?.dispose(); } @@ -114,6 +143,9 @@ export function PlayComponent() { onMount(() => { + const canvasElement = canvas(); + if(!canvasElement) return; + setupPointerLockListener(); video = document.createElement("video"); video.style.visibility = "hidden"; @@ -123,9 +155,17 @@ export function PlayComponent() { setHasStream(true); setShowOffline(false); await handleVideoInput(); - } else { + } 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(); } }); }); @@ -136,14 +176,70 @@ export function PlayComponent() { }); return ( - <> + {showOffline() ? (
Offline +
) : ( )} - + + +
+ ); + } + + interface ModalProps { + show: () => boolean; + setShow: (value: boolean) => void; + closeOnBackdropClick?: boolean; + handleVideoInput?: () => Promise; + lockPlay?: () => Promise; + } + + function Modal(props: ModalProps) { + return ( + +
props.closeOnBackdropClick && props.setShow(false)} + > + +
+
); } \ No newline at end of file From a727a9b710a629c952fed7a67b6719b8c809b2fc Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 21:07:13 +0100 Subject: [PATCH 04/11] some wip with styles --- packages/www/src/pages/play.tsx | 95 ++++++++++++++++++++++++++------- packages/www/src/ui/theme.ts | 9 +++- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 8c9dbb25..5bc4712e 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,9 +1,36 @@ import { Text } from "@nestri/www/ui/text"; -import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"; +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 { theme } from "../ui/theme"; +const Canvas = styled("canvas", { + base: { + aspectRatio: 16 / 9, + width: "100%", + height: "100%", + objectFit: "contain", + maxHeight: "100vh", + }}); + + const ModalContainer = styled("div", { + base: { + width: "100%", + maxWidth: 370, + maxHeight: "75vh", + borderRadius: 12, + borderWidth: 1, + borderStyle: "solid", + borderColor: theme.color.gray.d400, + backgroundColor: theme.color.pink.d400, + boxShadow: theme.color.boxShadow, + backdropFilter: "blur(20px)", + padding: "20px 25px" + } + }) export function PlayComponent() { const params = useParams(); const id = params.id; @@ -149,7 +176,7 @@ export function PlayComponent() { setupPointerLockListener(); video = document.createElement("video"); video.style.visibility = "hidden"; - webrtc = new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => { + webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { if (video && mediaStream) { video.srcObject = mediaStream; setHasStream(true); @@ -175,15 +202,23 @@ export function PlayComponent() { nestriMouse?.dispose(); }); + const { Modal, openModal } = createModal(); + return ( - + <> + + + + /* {showOffline() ? (
Offline
) : ( - + )} -
+
*/ ); } @@ -203,19 +238,43 @@ export function PlayComponent() { lockPlay?: () => Promise; } + function createModal() { + const [open, setOpen] = createSignal(false); + + return { + openModal() { + setOpen(true); + }, + Modal() { + return ( + + +
+ + Hello from modal
+ +
+
+
+
+ ); + }, + }; + } + function Modal(props: ModalProps) { return ( - -
props.closeOnBackdropClick && props.setShow(false)} - > - -
- -
+ ); } \ No newline at end of file diff --git a/packages/www/src/ui/theme.ts b/packages/www/src/ui/theme.ts index 31c223b1..c51974ef 100644 --- a/packages/www/src/ui/theme.ts +++ b/packages/www/src/ui/theme.ts @@ -233,6 +233,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, @@ -248,7 +249,8 @@ const light = (() => { focusBorder, focusColor, d1000, - text + text, + boxShadow }; })() @@ -391,6 +393,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, @@ -406,7 +410,8 @@ const dark = (() => { focusBorder, focusColor, d1000, - text + text, + boxShadow }; })() From c994dc112c86abc3e661bcecbe0e3cf13a8be717 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 21:16:27 +0100 Subject: [PATCH 05/11] added new App changes and background for theme --- packages/www/src/App.tsx | 29 ++--------------------------- packages/www/src/ui/theme.ts | 9 +++++---- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 965e7da9..aad73ca6 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -6,7 +6,6 @@ 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 { TeamCreate } from './pages/new'; import { PlayComponent } from './pages/play'; import { styled } from "@macaron-css/solid"; import { useStorage } from './providers/account'; @@ -35,10 +34,10 @@ globalStyle("html", { // Hardcode colors "@media": { "(prefers-color-scheme: light)": { - backgroundColor: "hsla(0,0%,98%)", + backgroundColor: "#f5f5f5", }, "(prefers-color-scheme: dark)": { - backgroundColor: "hsla(0,0%,0%)", + backgroundColor: "#1e1e1e", }, }, }); @@ -91,32 +90,8 @@ export const App: Component = () => { {props.children} - // - // - // - // - // - // - // - // - // - // {props.children} - // - // - // - // - // - // - // - // )} > - {/* - - - - {WorkspaceRoute} */} - { } const background = { - d100: 'hsla(0,0%,100%)', - d200: 'hsla(0,0%,98%)' + d100: '#f5f5f5', + d200: 'oklch(from #f5f5f5 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)' }; const contrastFg = '#ffffff'; @@ -371,9 +371,10 @@ const dark = (() => { } const background = { - d100: 'hsla(0,0%,4%)', - d200: 'hsla(0,0%,0%)' + d200: '#171717', + d100: "oklch(from #171717 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)" }; + const contrastFg = '#ffffff'; const focusBorder = `0 0 0 1px ${grayAlpha.d600}, 0px 0px 0px 4px rgba(255,255,255,0.24)`; const focusColor = blue.d900 From 1e782385934ec684a1a84a2c1bc1eec72eab16c1 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 3 Mar 2025 23:36:18 +0300 Subject: [PATCH 06/11] fix: Colors --- packages/www/src/App.tsx | 9 +- packages/www/src/index.tsx | 8 +- packages/www/src/pages/play.tsx | 542 +++++++++++++++++--------------- packages/www/src/pages/test.tsx | 18 ++ 4 files changed, 309 insertions(+), 268 deletions(-) create mode 100644 packages/www/src/pages/test.tsx diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index aad73ca6..766feb33 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -14,6 +14,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: { @@ -87,12 +88,14 @@ export const App: Component = () => { ( - - {props.children} - + // + // {props.children} + props.children + // )} > + { diff --git a/packages/www/src/index.tsx b/packages/www/src/index.tsx index 0bada0f9..80a66cb8 100644 --- a/packages/www/src/index.tsx +++ b/packages/www/src/index.tsx @@ -8,7 +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"; +// import { ToastProvider, Toaster } from "solid-notifications"; const root = document.getElementById("root"); @@ -20,12 +20,12 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { render( () => ( - - + // + // - + // ), root! ); \ No newline at end of file diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 5bc4712e..66f7bc67 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,11 +1,11 @@ import { Text } from "@nestri/www/ui/text"; -import { createSignal, createEffect, onCleanup, onMount, Show} from "solid-js"; +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 { theme } from "../ui/theme"; +import { lightClass, theme, darkClass } from "@nestri/www/ui/theme"; const Canvas = styled("canvas", { base: { @@ -14,289 +14,309 @@ const Canvas = styled("canvas", { height: "100%", objectFit: "contain", maxHeight: "100vh", - }}); - - const ModalContainer = styled("div", { - base: { - width: "100%", - maxWidth: 370, - maxHeight: "75vh", - borderRadius: 12, - borderWidth: 1, - borderStyle: "solid", - borderColor: theme.color.gray.d400, - backgroundColor: theme.color.pink.d400, - boxShadow: theme.color.boxShadow, - backdropFilter: "blur(20px)", - padding: "20px 25px" - } - }) + } +}); + +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.pink.d400, + backgroundColor: theme.color.red.d300, + // boxShadow: theme.color.boxShadow, + // backdropFilter: "blur(20px)", + padding: "20px 25px" + } +}) + 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 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(undefined); + let video: HTMLVideoElement; + let webrtc: WebRTCStream; + let nestriMouse: Mouse, nestriKeyboard: Keyboard; + + 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 [canvas, setCanvas] = createSignal(undefined); - let video: HTMLVideoElement; - let webrtc: WebRTCStream; - let nestriMouse: Mouse , nestriKeyboard: Keyboard; - - 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 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); }; - - /*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 { - - if (!showBannerModal) { - const playing = sessionStorage.getItem("showedBanner"); - setShowBannerModal(!playing || playing !== "true"); - setShowButtonModal(playing === "false"); - } + + 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(); - nestriKeyboard?.dispose(); - nestriMouse?.dispose(); - } - }); - }; + if (document.fullscreenElement !== null) { + if ('keyboard' in navigator && 'lock' in (navigator.keyboard as any)) { + const keys = [ + "AltLeft", "AltRight", "Tab", "Escape", + "ContextMenu", "MetaLeft", "MetaRight" + ]; - 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); + 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); + } } - }; - - - onMount(() => { + } + + } catch (error) { + console.error("Error during lock sequence:", error); + } + }; + + const setupPointerLockListener = () => { + document.addEventListener("pointerlockchange", () => { const canvasElement = canvas(); - if(!canvasElement) return; - - setupPointerLockListener(); - video = document.createElement("video"); - video.style.visibility = "hidden"; - webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { - if (video && mediaStream) { - video.srcObject = mediaStream; - setHasStream(true); - setShowOffline(false); - 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(); + if (!canvasElement) return; + if (document.pointerLockElement === canvasElement) { + initializeInputDevices(); + } else { + + if (!showBannerModal) { + const playing = sessionStorage.getItem("showedBanner"); + setShowBannerModal(!playing || playing !== "true"); + setShowButtonModal(playing === "false"); } - }); + + + + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + } }); - - onCleanup(() => { - 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("http://192.168.1.200:8088", id, async (mediaStream) => { + if (video && mediaStream) { + video.srcObject = mediaStream; + setHasStream(true); + setShowOffline(false); + 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(); + } }); - - const { Modal, openModal } = createModal(); + }); - return ( - <> + onCleanup(() => { + nestriKeyboard?.dispose(); + nestriMouse?.dispose(); + }); + + const { Modal, openModal } = createModal(); + + return ( + <> - /* - {showOffline() ? ( -
- Offline - -
- ) : ( - - )} - - -
*/ - ); - } + /* + {showOffline() ? ( +
+ Offline + +
+ ) : ( + + )} + + +
*/ + ); +} + +interface ModalProps { + show: () => boolean; + setShow: (value: boolean) => void; + closeOnBackdropClick?: boolean; + handleVideoInput?: () => Promise; + lockPlay?: () => Promise; +} + +function createModal() { + const [open, setOpen] = createSignal(false); + const [theme, setTheme] = createSignal( + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + ); + + const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); + const setColorScheme = (e: MediaQueryListEvent) => { + setTheme(e.matches ? "dark" : "light"); + }; + darkMode.addEventListener("change", setColorScheme); + onCleanup(() => { + darkMode.removeEventListener("change", setColorScheme); + }); - interface ModalProps { - show: () => boolean; - setShow: (value: boolean) => void; - closeOnBackdropClick?: boolean; - handleVideoInput?: () => Promise; - lockPlay?: () => Promise; - } - function createModal() { - const [open, setOpen] = createSignal(false); - - return { - openModal() { - setOpen(true); - }, - Modal() { - return ( - - -
+ +
- - Hello from modal
- -
-
-
- - ); - }, - }; - } + > + + Hello from modal
+ +
+
+
+
+ ); + }, + }; +} + +function Modal(props: ModalProps) { + return ( - function Modal(props: ModalProps) { - return ( - - - e.stopPropagation()} // Prevent closing when clicking inside modal + + e.stopPropagation()} // Prevent closing when clicking inside modal + > +
+
+ - -
-
-
- ); - } \ No newline at end of file + Continue Playing + + + + +
+ ); +} \ No newline at end of file diff --git a/packages/www/src/pages/test.tsx b/packages/www/src/pages/test.tsx new file mode 100644 index 00000000..f6c7c4bb --- /dev/null +++ b/packages/www/src/pages/test.tsx @@ -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 ( + + ) +} \ No newline at end of file From 4fd339b55feeb747ec7c9e03f66515fe37f04075 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 3 Mar 2025 23:42:58 +0300 Subject: [PATCH 07/11] fix: Have a root component --- packages/www/src/App.tsx | 43 ++++++++++++--------------------- packages/www/src/pages/play.tsx | 35 +++++++++------------------ packages/www/src/ui/root.tsx | 40 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 packages/www/src/ui/root.tsx diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 766feb33..1b960f0f 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -15,19 +15,20 @@ 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'; +import Root from './ui/root'; -const Root = styled("div", { - base: { - inset: 0, - lineHeight: 1, - fontSynthesis: "none", - color: theme.color.d1000.gray, - fontFamily: theme.font.family.body, - textRendering: "optimizeLegibility", - WebkitFontSmoothing: "antialised", - backgroundColor: theme.color.background.d100, - }, -}); +// const Root = styled("div", { +// base: { +// inset: 0, +// lineHeight: 1, +// fontSynthesis: "none", +// color: theme.color.d1000.gray, +// fontFamily: theme.font.family.body, +// textRendering: "optimizeLegibility", +// WebkitFontSmoothing: "antialised", +// backgroundColor: theme.color.background.d100, +// }, +// }); globalStyle("html", { fontSize: 16, @@ -65,25 +66,11 @@ globalStyle("*", { }); export const App: Component = () => { - const [theme, setTheme] = createSignal( - window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light", - ); - - const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); - const setColorScheme = (e: MediaQueryListEvent) => { - setTheme(e.matches ? "dark" : "light"); - }; - darkMode.addEventListener("change", setColorScheme); - onCleanup(() => { - darkMode.removeEventListener("change", setColorScheme); - }); - + const storage = useStorage(); return ( - + ( - window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light", - ); - - const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); - const setColorScheme = (e: MediaQueryListEvent) => { - setTheme(e.matches ? "dark" : "light"); - }; - darkMode.addEventListener("change", setColorScheme); - onCleanup(() => { - darkMode.removeEventListener("change", setColorScheme); - }); - return { openModal() { @@ -268,21 +254,22 @@ function createModal() { return ( -
+
- - Hello from modal
- -
-
+ > + + Hello from modal
+ +
+
+
); diff --git a/packages/www/src/ui/root.tsx b/packages/www/src/ui/root.tsx new file mode 100644 index 00000000..7e06d7a8 --- /dev/null +++ b/packages/www/src/ui/root.tsx @@ -0,0 +1,40 @@ +import { createSignal, JSX, onCleanup } from "solid-js"; +import { useStorage } from "@nestri/www/providers/account"; +import { darkClass, lightClass, theme } from "./theme"; +import { styled } from "@macaron-css/solid"; + +const BaseComponent = styled("div", { + base: { + inset: 0, + lineHeight: 1, + fontSynthesis: "none", + color: theme.color.d1000.gray, + fontFamily: theme.font.family.body, + textRendering: "optimizeLegibility", + WebkitFontSmoothing: "antialised", + backgroundColor: theme.color.background.d100, + }, +}); + +export default function Root(props: { children: number | boolean | Node | JSX.ArrayElement | (string & {}) | null | undefined; }) { + const [theme, setTheme] = createSignal( + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + ); + + const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); + const setColorScheme = (e: MediaQueryListEvent) => { + setTheme(e.matches ? "dark" : "light"); + }; + darkMode.addEventListener("change", setColorScheme); + onCleanup(() => { + darkMode.removeEventListener("change", setColorScheme); + }); + + return ( + + {props.children} + + ) +} \ No newline at end of file From fb0cb0b6cae59c81a0f4c60b49606df53fb9607e Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Mon, 3 Mar 2025 23:47:00 +0300 Subject: [PATCH 08/11] fix: Portal mount --- packages/www/src/App.tsx | 43 +++++++++++++++++++++------------ packages/www/src/pages/play.tsx | 5 +--- packages/www/src/ui/root.tsx | 40 ------------------------------ 3 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 packages/www/src/ui/root.tsx diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index 1b960f0f..766feb33 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -15,20 +15,19 @@ 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'; -import Root from './ui/root'; -// const Root = styled("div", { -// base: { -// inset: 0, -// lineHeight: 1, -// fontSynthesis: "none", -// color: theme.color.d1000.gray, -// fontFamily: theme.font.family.body, -// textRendering: "optimizeLegibility", -// WebkitFontSmoothing: "antialised", -// backgroundColor: theme.color.background.d100, -// }, -// }); +const Root = styled("div", { + base: { + inset: 0, + lineHeight: 1, + fontSynthesis: "none", + color: theme.color.d1000.gray, + fontFamily: theme.font.family.body, + textRendering: "optimizeLegibility", + WebkitFontSmoothing: "antialised", + backgroundColor: theme.color.background.d100, + }, +}); globalStyle("html", { fontSize: 16, @@ -66,11 +65,25 @@ globalStyle("*", { }); export const App: Component = () => { - + const [theme, setTheme] = createSignal( + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + ); + + const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); + const setColorScheme = (e: MediaQueryListEvent) => { + setTheme(e.matches ? "dark" : "light"); + }; + darkMode.addEventListener("change", setColorScheme); + onCleanup(() => { + darkMode.removeEventListener("change", setColorScheme); + }); + const storage = useStorage(); return ( - + + -
setOpen(false)}>close modal
-
); diff --git a/packages/www/src/ui/root.tsx b/packages/www/src/ui/root.tsx deleted file mode 100644 index 7e06d7a8..00000000 --- a/packages/www/src/ui/root.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createSignal, JSX, onCleanup } from "solid-js"; -import { useStorage } from "@nestri/www/providers/account"; -import { darkClass, lightClass, theme } from "./theme"; -import { styled } from "@macaron-css/solid"; - -const BaseComponent = styled("div", { - base: { - inset: 0, - lineHeight: 1, - fontSynthesis: "none", - color: theme.color.d1000.gray, - fontFamily: theme.font.family.body, - textRendering: "optimizeLegibility", - WebkitFontSmoothing: "antialised", - backgroundColor: theme.color.background.d100, - }, -}); - -export default function Root(props: { children: number | boolean | Node | JSX.ArrayElement | (string & {}) | null | undefined; }) { - const [theme, setTheme] = createSignal( - window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light", - ); - - const darkMode = window.matchMedia("(prefers-color-scheme: dark)"); - const setColorScheme = (e: MediaQueryListEvent) => { - setTheme(e.matches ? "dark" : "light"); - }; - darkMode.addEventListener("change", setColorScheme); - onCleanup(() => { - darkMode.removeEventListener("change", setColorScheme); - }); - - return ( - - {props.children} - - ) -} \ No newline at end of file From 402e89422443a2825f0209da0c4c0f67b92b78c3 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Mon, 3 Mar 2025 22:28:53 +0100 Subject: [PATCH 09/11] added modal with correct styling. Open: need to wire the modals correctly and have the welcome modal --- packages/www/src/pages/play.tsx | 126 +++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 35 deletions(-) diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 067dbbcf..6df0e5ca 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -1,3 +1,5 @@ +// 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"; @@ -23,18 +25,33 @@ const ModalContainer = styled("div", { maxWidth: 370, maxHeight: "75vh", height: "auto", - // borderRadius: 12, - // borderWidth: 1, - // borderStyle: "solid", - // borderColor: theme.color.gray.d400, - // backgroundColor: theme.color.pink.d400, - backgroundColor: theme.color.red.d300, - // boxShadow: theme.color.boxShadow, - // backdropFilter: "blur(20px)", + 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" } }) +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; @@ -54,6 +71,8 @@ export function PlayComponent() { let webrtc: WebRTCStream; let nestriMouse: Mouse, nestriKeyboard: Keyboard; + const { Modal, openModal } = createModal(); + const initializeInputDevices = () => { const canvasElement = canvas(); if (!canvasElement || !webrtc) return; @@ -134,7 +153,10 @@ export function PlayComponent() { if (!showBannerModal) { const playing = sessionStorage.getItem("showedBanner"); setShowBannerModal(!playing || playing !== "true"); - setShowButtonModal(playing === "false"); + if(!playing) { + openModal(); + } + } @@ -206,31 +228,21 @@ export function PlayComponent() { nestriMouse?.dispose(); }); - const { Modal, openModal } = createModal(); - - return ( - <> - - - - /* + return ( {showOffline() ? (
Offline -
) : ( )} - -
*/ + +
); } @@ -242,6 +254,44 @@ interface ModalProps { lockPlay?: () => Promise; } +function createWelcomeModal() { + const [open, setOpen] = createSignal(false); + + return { + openWelcomeModal() { + setOpen(true); + }, + WelcomeModal(props: ModalProps) { + return ( + + +
+ +
+ Happy that you use Nestri! + +
+
+
+
+
+ ); + }, + }; +} + function createModal() { const [open, setOpen] = createSignal(false); @@ -249,24 +299,30 @@ function createModal() { openModal() { setOpen(true); }, - Modal() { + Modal(props: ModalProps) { return ( -
- - Hello from modal
- -
-
+ > + +
+ + +
+
+
); From b734892c55370bac1e00ea145a2b360dcddf354e Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Sun, 9 Mar 2025 14:53:11 +0100 Subject: [PATCH 10/11] extracted modal component + showing modal on enter and mouspointer loss --- packages/www/src/components/Modal.tsx | 80 ++++++++++ packages/www/src/pages/play.tsx | 208 +++++++++++--------------- 2 files changed, 166 insertions(+), 122 deletions(-) create mode 100644 packages/www/src/components/Modal.tsx diff --git a/packages/www/src/components/Modal.tsx b/packages/www/src/components/Modal.tsx new file mode 100644 index 00000000..5886bbe4 --- /dev/null +++ b/packages/www/src/components/Modal.tsx @@ -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 = (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 ( + + +
{ + if (e.target === e.currentTarget && props.onClose) { + if (props.onClose.length > 0) { + (props.onClose as (value: boolean) => void)(false); + } else { + (props.onClose as () => void)(); + } + } + }} + > + + {props.children} + +
+
+
+ ); +}; diff --git a/packages/www/src/pages/play.tsx b/packages/www/src/pages/play.tsx index 6df0e5ca..91ba5102 100644 --- a/packages/www/src/pages/play.tsx +++ b/packages/www/src/pages/play.tsx @@ -8,6 +8,7 @@ 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: { @@ -19,22 +20,7 @@ const Canvas = styled("canvas", { } }); -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" - } -}) + const Button = styled("button", { base: { @@ -72,6 +58,7 @@ export function PlayComponent() { let nestriMouse: Mouse, nestriKeyboard: Keyboard; const { Modal, openModal } = createModal(); + const { WelcomeModal, openWelcomeModal } = createWelcomeModal(); const initializeInputDevices = () => { const canvasElement = canvas(); @@ -149,14 +136,17 @@ export function PlayComponent() { if (document.pointerLockElement === canvasElement) { initializeInputDevices(); } else { - - if (!showBannerModal) { + 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"); - if(!playing) { + openWelcomeModal(); + + if (playing) { + setShowButtonModal(true); openModal(); } - } @@ -202,11 +192,27 @@ export function PlayComponent() { setupPointerLockListener(); video = document.createElement("video"); video.style.visibility = "hidden"; - webrtc = new WebRTCStream("http://192.168.1.200:8088", id, async (mediaStream) => { + 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"); @@ -229,20 +235,27 @@ export function PlayComponent() { }); return ( - {showOffline() ? ( -
- Offline -
- ) : ( - - )} + {showOffline() ? ( +
+ Offline +
+ ) : ( + + )} + + -
+ setShow={setShowButtonModal} + closeOnBackdropClick={false} + handleVideoInput={handleVideoInput} + lockPlay={lockPlay} /> + ); } @@ -254,109 +267,60 @@ interface ModalProps { lockPlay?: () => Promise; } +type GameModalProps = ModalProps & { + handleVideoInput?: () => Promise; + lockPlay?: () => Promise; + setShow?: (show: boolean) => void; +} + function createWelcomeModal() { - const [open, setOpen] = createSignal(false); + const controller = createModalController(); return { - openWelcomeModal() { - setOpen(true); - }, - WelcomeModal(props: ModalProps) { + openWelcomeModal: controller.open, + WelcomeModal(props: GameModalProps) { return ( - - -
- -
- Happy that you use Nestri! - -
-
-
-
-
+ +
+ Happy that you use Nestri! + +
+
); }, }; } function createModal() { - const [open, setOpen] = createSignal(false); + const controller = createModalController(); return { - openModal() { - setOpen(true); - }, - Modal(props: ModalProps) { + openModal: controller.open, + Modal(props: GameModalProps) { return ( - - -
- -
- - -
-
-
-
-
+ +
+ + +
+
); }, }; -} - -function Modal(props: ModalProps) { - return ( - - - e.stopPropagation()} // Prevent closing when clicking inside modal - > -
-
- - -
-
-
- ); } \ No newline at end of file From 5189bf768a73f5a27d0c31387e909df78efedfd4 Mon Sep 17 00:00:00 2001 From: AquaWolf <3daquawolf@gmail.com> Date: Sat, 15 Mar 2025 21:06:30 +0100 Subject: [PATCH 11/11] merge --- package.json | 23 +- packages/core/package.json | 1 + packages/core/src/examples.ts | 8 +- packages/core/src/polar.ts | 8 - packages/core/src/polar/index.ts | 169 ++++++++++ packages/core/src/polar/polar.sql.ts | 22 ++ packages/core/src/team/index.ts | 23 +- packages/core/src/user/index.ts | 12 +- packages/functions/package.json | 2 +- packages/functions/src/api/account.ts | 8 +- packages/server/Cargo.lock | 6 +- packages/www/index.html | 4 +- packages/www/package.json | 8 +- packages/www/src/App.tsx | 11 +- packages/www/src/pages/default-state.tsx | 7 - packages/www/src/pages/new.tsx | 165 ++++++++- .../www/src/providers/{auth.tsx => auth.ts} | 6 +- packages/www/src/ui/button.ts | 34 ++ packages/www/src/ui/form.tsx | 316 ++++++++++++++++++ packages/www/src/ui/index.ts | 6 + packages/www/src/ui/layout.tsx | 108 ++++-- packages/www/src/ui/text.tsx | 15 +- packages/www/src/ui/theme.ts | 30 +- 23 files changed, 890 insertions(+), 102 deletions(-) delete mode 100644 packages/core/src/polar.ts create mode 100644 packages/core/src/polar/index.ts create mode 100644 packages/core/src/polar/polar.sql.ts delete mode 100644 packages/www/src/pages/default-state.tsx rename packages/www/src/providers/{auth.tsx => auth.ts} (96%) create mode 100644 packages/www/src/ui/button.ts create mode 100644 packages/www/src/ui/form.tsx create mode 100644 packages/www/src/ui/index.ts diff --git a/package.json b/package.json index 407f82af..31c08650 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,5 @@ { "name": "nestri", - "private": true, - "scripts": { - "build": "turbo build", - "dev": "turbo dev", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "lint": "turbo lint", - "sso": "aws sso login --sso-session=nestri --no-browser --use-device-code" - }, "devDependencies": { "@cloudflare/workers-types": "4.20240821.1", "@pulumi/pulumi": "^3.134.0", @@ -18,16 +10,21 @@ "engines": { "node": ">=18" }, - "packageManager": "bun@1.1.18", - "workspaces": [ - "apps/*", - "packages/*" - ], + "packageManager": "bun@1.2.4", + "private": true, + "scripts": { + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "sso": "aws sso login --sso-session=nestri --no-browser --use-device-code" + }, "trustedDependencies": [ "core-js-pure", "esbuild", "workerd" ], + "workspaces": [ + "apps/*", + "packages/*" + ], "dependencies": { "sst": "3.9.1" } diff --git a/packages/core/package.json b/packages/core/package.json index 67488454..5e7cd610 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ "@aws-sdk/client-sesv2": "^3.753.0", "@instantdb/admin": "^0.17.7", "@neondatabase/serverless": "^0.10.4", + "@openauthjs/openauth": "0.4.3", "@openauthjs/openevent": "^0.0.27", "@polar-sh/sdk": "^0.26.1", "drizzle-orm": "^0.39.3", diff --git a/packages/core/src/examples.ts b/packages/core/src/examples.ts index bb59bb93..793ac7f3 100644 --- a/packages/core/src/examples.ts +++ b/packages/core/src/examples.ts @@ -1,4 +1,3 @@ -import { teamID } from "./drizzle/types"; import { prefixes } from "./utils"; export module Examples { export const Id = (prefix: keyof typeof prefixes) => @@ -18,7 +17,7 @@ export module Examples { name: "John Does' Team", slug: "john_doe", } - + export const Member = { id: Id("member"), email: "john@example.com", @@ -26,4 +25,9 @@ export module Examples { timeSeen: new Date("2025-02-23T13:39:52.249Z"), } + export const Polar = { + teamID: Id("team"), + timeSeen: new Date("2025-02-23T13:39:52.249Z"), + } + } \ No newline at end of file diff --git a/packages/core/src/polar.ts b/packages/core/src/polar.ts deleted file mode 100644 index 660aff52..00000000 --- a/packages/core/src/polar.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Resource } from "sst"; -import { Polar as PolarSdk } from "@polar-sh/sdk"; - -const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" }); - -export module Polar { - export const client = polar; -} \ No newline at end of file diff --git a/packages/core/src/polar/index.ts b/packages/core/src/polar/index.ts new file mode 100644 index 00000000..36c214a6 --- /dev/null +++ b/packages/core/src/polar/index.ts @@ -0,0 +1,169 @@ +import { z } from "zod"; +import { fn } from "../utils"; +import { Resource } from "sst"; +import { eq, and } from "../drizzle"; +import { useTeam } from "../actor"; +import { createEvent } from "../event"; +import { polarTable, Standing } from "./polar.sql"; +import { Polar as PolarSdk } from "@polar-sh/sdk"; +import { useTransaction } from "../drizzle/transaction"; + +const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" }); + +export module Polar { + export const client = polar; + + export const Info = z.object({ + teamID: z.string(), + customerID: z.string(), + subscriptionID: z.string().nullable(), + subscriptionItemID: z.string().nullable(), + standing: z.enum(Standing), + }); + + export type Info = z.infer; + + export const Checkout = z.object({ + annual: z.boolean().optional(), + successUrl: z.string(), + cancelUrl: z.string(), + }); + + export const CheckoutSession = z.object({ + url: z.string().nullable(), + }); + + export const CustomerSubscriptionEventType = [ + "created", + "updated", + "deleted", + ] as const; + + export const Events = { + CustomerSubscriptionEvent: createEvent( + "polar.customer-subscription-event", + z.object({ + type: z.enum(CustomerSubscriptionEventType), + status: z.string(), + teamID: z.string().min(1), + customerID: z.string().min(1), + subscriptionID: z.string().min(1), + subscriptionItemID: z.string().min(1), + }), + ), + }; + + export function get() { + return useTransaction(async (tx) => + tx + .select() + .from(polarTable) + .where(eq(polarTable.teamID, useTeam())) + .execute() + .then((rows) => rows.map(serialize).at(0)), + ); + } + + export const fromUserEmail = fn(z.string().min(1), async (email) => { + try { + const customers = await client.customers.list({ email }) + + if (customers.result.items.length === 0) { + return await client.customers.create({ email }) + } else { + return customers.result.items[0] + } + + } catch (err) { + //FIXME: This is the issue [Polar.sh/#5147](https://github.com/polarsource/polar/issues/5147) + // console.log("error", err) + return undefined + } + }) + + export const setCustomerID = fn(Info.shape.customerID, async (customerID) => + useTransaction(async (tx) => + tx + .insert(polarTable) + .values({ + teamID: useTeam(), + customerID, + standing: "new", + }) + .execute(), + ), + ); + + export const setSubscription = fn( + Info.pick({ + subscriptionID: true, + subscriptionItemID: true, + }), + (input) => + useTransaction(async (tx) => + tx + .update(polarTable) + .set({ + subscriptionID: input.subscriptionID, + subscriptionItemID: input.subscriptionItemID, + }) + .where(eq(polarTable.teamID, useTeam())) + .returning() + .execute() + .then((rows) => rows.map(serialize).at(0)), + ), + ); + + export const removeSubscription = fn( + z.string().min(1), + (stripeSubscriptionID) => + useTransaction((tx) => + tx + .update(polarTable) + .set({ + subscriptionItemID: null, + subscriptionID: null, + }) + .where(and(eq(polarTable.subscriptionID, stripeSubscriptionID))) + .execute(), + ), + ); + + export const setStanding = fn( + Info.pick({ + subscriptionID: true, + standing: true, + }), + (input) => + useTransaction((tx) => + tx + .update(polarTable) + .set({ standing: input.standing }) + .where(and(eq(polarTable.subscriptionID, input.subscriptionID!))) + .execute(), + ), + ); + + export const fromCustomerID = fn(Info.shape.customerID, (customerID) => + useTransaction((tx) => + tx + .select() + .from(polarTable) + .where(and(eq(polarTable.customerID, customerID))) + .execute() + .then((rows) => rows.map(serialize).at(0)), + ), + ); + + function serialize( + input: typeof polarTable.$inferSelect, + ): z.infer { + return { + teamID: input.teamID, + customerID: input.customerID, + subscriptionID: input.subscriptionID, + subscriptionItemID: input.subscriptionItemID, + standing: input.standing, + }; + } +} \ No newline at end of file diff --git a/packages/core/src/polar/polar.sql.ts b/packages/core/src/polar/polar.sql.ts new file mode 100644 index 00000000..6e28748c --- /dev/null +++ b/packages/core/src/polar/polar.sql.ts @@ -0,0 +1,22 @@ +import { timestamps, teamID } from "../drizzle/types"; +import { teamIndexes, teamTable } from "../team/team.sql"; +import { pgTable, text, varchar } from "drizzle-orm/pg-core"; + +export const Standing = ["new", "good", "overdue"] as const; + +export const polarTable = pgTable( + "polar", + { + teamID: teamID.teamID.primaryKey().references(() => teamTable.id), + ...timestamps, + customerID: varchar("customer_id", { length: 255 }).notNull(), + subscriptionID: varchar("subscription_id", { length: 255 }), + subscriptionItemID: varchar("subscription_item_id", { + length: 255, + }), + standing: text("standing", { enum: Standing }).notNull(), + }, + (table) => ({ + ...teamIndexes(table), + }) +) \ No newline at end of file diff --git a/packages/core/src/team/index.ts b/packages/core/src/team/index.ts index 1db157bc..3d7a882e 100644 --- a/packages/core/src/team/index.ts +++ b/packages/core/src/team/index.ts @@ -3,13 +3,13 @@ import { Resource } from "sst"; import { bus } from "sst/aws/bus"; import { Common } from "../common"; import { createID, fn } from "../utils"; -import { VisibleError } from "../error"; import { Examples } from "../examples"; import { teamTable } from "./team.sql"; import { createEvent } from "../event"; -import { assertActor } from "../actor"; +import { assertActor, withActor } from "../actor"; import { and, eq, sql } from "../drizzle"; import { memberTable } from "../member/member.sql"; +import { HTTPException } from 'hono/http-exception'; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; export module Team { @@ -45,11 +45,11 @@ export module Team { ), }; - export class WorkspaceExistsError extends VisibleError { + export class TeamExistsError extends HTTPException { constructor(slug: string) { super( - "team.slug_exists", - `there is already a workspace named "${slug}"`, + 400, + { message: `There is already a team named "${slug}"`, } ); } } @@ -65,15 +65,16 @@ export module Team { slug: input.slug, name: input.name }) - .onConflictDoNothing() - .returning({ insertedID: teamTable.id }) + .onConflictDoNothing({ target: teamTable.slug }) - if (result.length === 0) throw new WorkspaceExistsError(input.slug); + if (!result.rowCount) throw new TeamExistsError(input.slug); await afterTx(() => - bus.publish(Resource.Bus, Events.Created, { - teamID: id, - }), + withActor({ type: "system", properties: { teamID: id } }, () => + bus.publish(Resource.Bus, Events.Created, { + teamID: id, + }) + ), ); return id; }) diff --git a/packages/core/src/user/index.ts b/packages/core/src/user/index.ts index 439e1590..a27f1ad2 100644 --- a/packages/core/src/user/index.ts +++ b/packages/core/src/user/index.ts @@ -1,4 +1,6 @@ import { z } from "zod"; +import { Polar } from "../polar"; +import { Team } from "../team"; import { bus } from "sst/aws/bus"; import { Common } from "../common"; import { createID, fn } from "../utils"; @@ -11,7 +13,6 @@ import { assertActor, withActor } from "../actor"; import { memberTable } from "../member/member.sql"; import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle"; import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction"; -import { Team } from "../team"; export module User { @@ -106,12 +107,8 @@ export module User { //FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake - // const customer = await Polar.client.customers.create({ - // email: input.email, - // metadata: { - // userID, - // }, - // }); + const customer = await Polar.fromUserEmail(input.email) + console.log("customer", customer) const name = sanitizeUsername(input.name); @@ -131,6 +128,7 @@ export module User { avatarUrl: input.avatarUrl, email: input.email, discriminator: Number(discriminator), + polarCustomerID: customer?.id }) await afterTx(() => withActor({ diff --git a/packages/functions/package.json b/packages/functions/package.json index 29ed020f..88e2baa4 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -14,7 +14,7 @@ "typescript": "^5.0.0" }, "dependencies": { - "@openauthjs/openauth": "^0.3.9", + "@openauthjs/openauth": "0.4.3", "hono": "^4.6.15", "hono-openapi": "^0.3.1", "partysocket": "1.0.3" diff --git a/packages/functions/src/api/account.ts b/packages/functions/src/api/account.ts index 32ce1de7..c5ab6163 100644 --- a/packages/functions/src/api/account.ts +++ b/packages/functions/src/api/account.ts @@ -42,8 +42,10 @@ export module AccountApi { }), async (c) => { const actor = assertActor("user"); - const currentUser = await User.fromID(actor.properties.userID) - if (!currentUser) return c.json({ error: "This account does not exist, it may have been deleted" }, 404) + const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()]) + + if (!currentUser) return c.json({ error: "This account does not exist; it may have been deleted" }, 404) + const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser return c.json({ @@ -51,10 +53,10 @@ export module AccountApi { id, email, name, + teams, avatarUrl, discriminator, polarCustomerID, - teams: await User.teams(), } }, 200); }, diff --git a/packages/server/Cargo.lock b/packages/server/Cargo.lock index eca99bbb..579948cc 100644 --- a/packages/server/Cargo.lock +++ b/packages/server/Cargo.lock @@ -1876,7 +1876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -2668,9 +2668,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", diff --git a/packages/www/index.html b/packages/www/index.html index 3ec7a691..536f6e06 100644 --- a/packages/www/index.html +++ b/packages/www/index.html @@ -9,12 +9,12 @@ { ( - // - // {props.children} - props.children - // + + {props.children} + + // props.children )} > - + { diff --git a/packages/www/src/pages/default-state.tsx b/packages/www/src/pages/default-state.tsx deleted file mode 100644 index c13befc0..00000000 --- a/packages/www/src/pages/default-state.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function DefaultState() { - return ( -
- We are logging you in -
- ) -} \ No newline at end of file diff --git a/packages/www/src/pages/new.tsx b/packages/www/src/pages/new.tsx index 3319de5b..4b926763 100644 --- a/packages/www/src/pages/new.tsx +++ b/packages/www/src/pages/new.tsx @@ -1,15 +1,168 @@ +import * as v from "valibot" +import { styled } from "@macaron-css/solid"; import { Text } from "@nestri/www/ui/text"; +import { utility } from "@nestri/www/ui/utility"; +import { theme } from "@nestri/www/ui/theme"; +import { FormField, Input, Select } from "@nestri/www/ui/form"; import { Container, FullScreen } from "@nestri/www/ui/layout"; +import { createForm, required, email, valiForm } from "@modular-forms/solid"; +import { Button } from "@nestri/www/ui"; + +// const nameRegex = /^[a-z]+$/ + +const FieldList = styled("div", { + base: { + width: "100%", + maxWidth: 380, + ...utility.stack(5), + }, +}); + +const Hr = styled("hr", { + base: { + border: 0, + backgroundColor: theme.color.gray.d400, + width: "100%", + height: 1, + } +}) + +const Plan = { + Pro: 'BYOG', + Basic: 'Hosted', +} as const; + +const schema = v.object({ + plan: v.pipe( + v.enum(Plan), + v.minLength(2,"Please choose a plan"), + ), + display_name: v.pipe( + v.string(), + v.maxLength(32, 'Please use 32 characters at maximum.'), + ), + slug: v.pipe( + v.string(), + v.minLength(2, 'Please use 2 characters at minimum.'), + // v.regex(nameRegex, "Use only small letters, no numbers or special characters"), + v.maxLength(48, 'Please use 48 characters at maximum.'), + ) +}) + +// const Details = styled("details", { +// base: { +// overflow: "hidden", +// transition: "max-height .2s ease" +// } +// }) + +// const Summary = styled("summary", { +// base: { +// userSelect: "none", +// cursor: "pointer", +// listStyle: "none" +// } +// }) + +// const SVG = styled("svg", { +// base: { +// color: theme.color.gray.d900, +// width: 20, +// height: 20, +// marginRight: theme.space[2] +// } +// }) + +// const Subtitle = styled("p", { +// base: { +// color: theme.color.gray.d900, +// fontSize: theme.font.size.sm, +// fontWeight: theme.font.weight.regular, +// lineHeight: "1rem" +// } +// }) + +export function CreateTeamComponent() { + const [form, { Form, Field }] = createForm({ + validate: valiForm(schema), + }); -export function TeamCreate() { return ( - - - Your first deploy is just a sign-up away. - - + + + + Create a Team + + + Choose something that your teammates will recognize + +
+
+
+ + + {(field, props) => ( + + + + )} + + + {(field, props) => ( + +