mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
extracted modal component + showing modal on enter and mouspointer loss
This commit is contained in:
80
packages/www/src/components/Modal.tsx
Normal file
80
packages/www/src/components/Modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { Keyboard, Mouse, WebRTCStream } from "@nestri/input";
|
|||||||
import { Container, FullScreen } from "@nestri/www/ui/layout";
|
import { Container, FullScreen } from "@nestri/www/ui/layout";
|
||||||
import { styled } from "@macaron-css/solid";
|
import { styled } from "@macaron-css/solid";
|
||||||
import { lightClass, theme, darkClass } from "@nestri/www/ui/theme";
|
import { lightClass, theme, darkClass } from "@nestri/www/ui/theme";
|
||||||
|
import { Modal, createModalController } from "../components/Modal";
|
||||||
|
|
||||||
const Canvas = styled("canvas", {
|
const Canvas = styled("canvas", {
|
||||||
base: {
|
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", {
|
const Button = styled("button", {
|
||||||
base: {
|
base: {
|
||||||
@@ -72,6 +58,7 @@ export function PlayComponent() {
|
|||||||
let nestriMouse: Mouse, nestriKeyboard: Keyboard;
|
let nestriMouse: Mouse, nestriKeyboard: Keyboard;
|
||||||
|
|
||||||
const { Modal, openModal } = createModal();
|
const { Modal, openModal } = createModal();
|
||||||
|
const { WelcomeModal, openWelcomeModal } = createWelcomeModal();
|
||||||
|
|
||||||
const initializeInputDevices = () => {
|
const initializeInputDevices = () => {
|
||||||
const canvasElement = canvas();
|
const canvasElement = canvas();
|
||||||
@@ -149,14 +136,17 @@ export function PlayComponent() {
|
|||||||
if (document.pointerLockElement === canvasElement) {
|
if (document.pointerLockElement === canvasElement) {
|
||||||
initializeInputDevices();
|
initializeInputDevices();
|
||||||
} else {
|
} else {
|
||||||
|
console.log("Pointer lock lost Show Banner Modal:", showBannerModal());
|
||||||
if (!showBannerModal) {
|
if (!showBannerModal()) {
|
||||||
|
console.log("Pointer lock lost, showing banner");
|
||||||
const playing = sessionStorage.getItem("showedBanner");
|
const playing = sessionStorage.getItem("showedBanner");
|
||||||
setShowBannerModal(!playing || playing !== "true");
|
setShowBannerModal(!playing || playing !== "true");
|
||||||
if(!playing) {
|
openWelcomeModal();
|
||||||
|
|
||||||
|
if (playing) {
|
||||||
|
setShowButtonModal(true);
|
||||||
openModal();
|
openModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -202,11 +192,27 @@ export function PlayComponent() {
|
|||||||
setupPointerLockListener();
|
setupPointerLockListener();
|
||||||
video = document.createElement("video");
|
video = document.createElement("video");
|
||||||
video.style.visibility = "hidden";
|
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) {
|
if (video && mediaStream) {
|
||||||
video.srcObject = mediaStream;
|
video.srcObject = mediaStream;
|
||||||
setHasStream(true);
|
setHasStream(true);
|
||||||
setShowOffline(false);
|
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();
|
await handleVideoInput();
|
||||||
} else if (mediaStream === null) {
|
} else if (mediaStream === null) {
|
||||||
console.log("MediaStream is null, Room is offline");
|
console.log("MediaStream is null, Room is offline");
|
||||||
@@ -234,9 +240,16 @@ export function PlayComponent() {
|
|||||||
<span class="text-2xl font-semibold flex items-center gap-2">Offline</span>
|
<span class="text-2xl font-semibold flex items-center gap-2">Offline</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Canvas ref={setCanvas} onClick={lockPlay}/>
|
<Canvas ref={setCanvas} onClick={lockPlay} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<WelcomeModal
|
||||||
|
show={showBannerModal}
|
||||||
|
setShow={setShowBannerModal}
|
||||||
|
closeOnBackdropClick={false}
|
||||||
|
handleVideoInput={handleVideoInput}
|
||||||
|
lockPlay={lockPlay} />
|
||||||
|
|
||||||
<Modal show={showButtonModal}
|
<Modal show={showButtonModal}
|
||||||
setShow={setShowButtonModal}
|
setShow={setShowButtonModal}
|
||||||
closeOnBackdropClick={false}
|
closeOnBackdropClick={false}
|
||||||
@@ -254,109 +267,60 @@ interface ModalProps {
|
|||||||
lockPlay?: () => Promise<void>;
|
lockPlay?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GameModalProps = ModalProps & {
|
||||||
|
handleVideoInput?: () => Promise<void>;
|
||||||
|
lockPlay?: () => Promise<void>;
|
||||||
|
setShow?: (show: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function createWelcomeModal() {
|
function createWelcomeModal() {
|
||||||
const [open, setOpen] = createSignal(false);
|
const controller = createModalController();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openWelcomeModal() {
|
openWelcomeModal: controller.open,
|
||||||
setOpen(true);
|
WelcomeModal(props: GameModalProps) {
|
||||||
},
|
|
||||||
WelcomeModal(props: ModalProps) {
|
|
||||||
return (
|
return (
|
||||||
<Portal mount={document.getElementById("styled")!}>
|
<Modal
|
||||||
<Show when={open()}>
|
isOpen={controller.isOpen()}
|
||||||
<div
|
onClose={controller.close}
|
||||||
style={`
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<ModalContainer>
|
|
||||||
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
|
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
|
||||||
Happy that you use Nestri!
|
Happy that you use Nestri!
|
||||||
<Button onClick={async () => {
|
<Button onClick={async () => {
|
||||||
sessionStorage.setItem("showedBanner", "true");
|
sessionStorage.setItem("showedBanner", "true");
|
||||||
await props.handleVideoInput?.();
|
await props.handleVideoInput?.();
|
||||||
await props.lockPlay?.();
|
await props.lockPlay?.();
|
||||||
|
controller.close();
|
||||||
}}>Let's go</Button>
|
}}>Let's go</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalContainer>
|
</Modal>
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModal() {
|
function createModal() {
|
||||||
const [open, setOpen] = createSignal(false);
|
const controller = createModalController();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openModal() {
|
openModal: controller.open,
|
||||||
setOpen(true);
|
Modal(props: GameModalProps) {
|
||||||
},
|
|
||||||
Modal(props: ModalProps) {
|
|
||||||
return (
|
return (
|
||||||
<Portal mount={document.getElementById("styled")!}>
|
<Modal
|
||||||
<Show when={open()}>
|
isOpen={controller.isOpen()}
|
||||||
<div
|
onClose={controller.close}
|
||||||
style={`
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<ModalContainer>
|
|
||||||
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
|
<div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
|
||||||
<Button onClick={async () => {
|
<Button onClick={async () => {
|
||||||
props.setShow(false);
|
props.setShow?.(false);
|
||||||
await props.handleVideoInput?.();
|
await props.handleVideoInput?.();
|
||||||
await props.lockPlay?.();
|
await props.lockPlay?.();
|
||||||
|
controller.close();
|
||||||
}}>Continue Playing</Button>
|
}}>Continue Playing</Button>
|
||||||
<Button onClick={() => setOpen(false)}>Shutdown Nestri</Button>
|
<Button onClick={controller.close}>Shutdown Nestri</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalContainer>
|
</Modal>
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal(props: ModalProps) {
|
|
||||||
return (
|
|
||||||
|
|
||||||
|
|
||||||
<ModalContainer
|
|
||||||
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside modal
|
|
||||||
>
|
|
||||||
<div class="size-full flex flex-col">
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<button
|
|
||||||
class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4"
|
|
||||||
onClick={async () => {
|
|
||||||
props.setShow(false);
|
|
||||||
sessionStorage.setItem("showedBanner", "true");
|
|
||||||
await props.handleVideoInput?.();
|
|
||||||
await props.lockPlay?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Continue Playing
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4"
|
|
||||||
>
|
|
||||||
Shutdown Nestri
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user