feat: more accessible full screen mode

This commit is contained in:
TrèsAbhi committed 2024-04-01 21:33:47 -05:00
1 parent d0d905ccfb
commit 5b889f8483
5 files changed
+301 -296

No files matched your search

@@ -99,184 +99,167 @@ export function AntagonistBar({ floating }: AntagonistBarProps) {
</Button> </Button>
<Flex align="center" gap="4"> <Flex align="center" gap="4">
{floating && ( <Dialog.Root
<Flex gap="2" align="center"> open={antagonistSelectorOpen}
Versus {antagonist.tank.name} onOpenChange={setAntagonistSelectorVisible}
<SmallTankIcon id={antagonist.tank.id} size={16} /> >
</Flex> <Dialog.Trigger>
)} <Button variant="ghost">
<Flex gap="2" align="center">
Versus {antagonist.tank.name}
<SmallTankIcon id={antagonist.tank.id} size={16} />
</Flex>
</Button>
</Dialog.Trigger>
{!floating && ( <Dialog.Content>
<Dialog.Root <Tabs.Root
open={antagonistSelectorOpen} value={tab}
onOpenChange={setAntagonistSelectorVisible} onValueChange={setTab}
> style={{ position: 'relative' }}
<Dialog.Trigger> >
<Button variant="ghost"> <Dialog.Close>
<Flex gap="2" align="center"> <Button
Versus {antagonist.tank.name} variant="ghost"
<SmallTankIcon id={antagonist.tank.id} size={16} /> style={{ position: 'absolute', right: 0, top: 8 }}
</Flex> >
</Button> <Cross1Icon />
</Dialog.Trigger> </Button>
</Dialog.Close>
<Dialog.Content> <Flex gap="4" direction="column">
<Tabs.Root <Tabs.List>
value={tab} <Tabs.Trigger value="search">Search</Tabs.Trigger>
onValueChange={setTab} <Tabs.Trigger value="configure">Configure</Tabs.Trigger>
style={{ position: 'relative' }} </Tabs.List>
>
<Dialog.Close> <Tabs.Content value="search">
<Button <Flex
variant="ghost" direction="column"
style={{ position: 'absolute', right: 0, top: 8 }} gap="4"
style={{ flex: 1 }}
justify="center"
> >
<Cross1Icon /> <TankSearch
</Button> compact
</Dialog.Close> onSelect={(tank) => {
mutateDuel((draft) => {
draft.antagonist!.tank = tank;
draft.antagonist!.engine = tank.engines.at(-1)!;
draft.antagonist!.track = tank.tracks.at(-1)!;
draft.antagonist!.turret = tank.turrets.at(-1)!;
draft.antagonist!.gun =
draft.antagonist!.turret.guns.at(-1)!;
draft.antagonist!.shell =
draft.antagonist!.gun.shells[0];
});
setAntagonistSelectorVisible(false);
}}
/>
</Flex>
</Tabs.Content>
<Flex gap="4" direction="column"> <Tabs.Content value="configure">
<Tabs.List> <Flex direction="column" gap="4">
<Tabs.Trigger value="search">Search</Tabs.Trigger> <Flex direction="column" gap="2" style={{ flex: 1 }}>
<Tabs.Trigger value="configure">Configure</Tabs.Trigger> <Heading size="4">
</Tabs.List> {antagonist.tank.name} modules
</Heading>
<Tabs.Content value="search"> <Flex gap="2" wrap="wrap">
<Flex <Flex>
direction="column" {antagonist.tank.turrets.map((turret, index) => (
gap="4" <Tooltip content={turret.name} key={turret.id}>
style={{ flex: 1 }} <ModuleButton
justify="center" rowChild
> first={index === 0}
<TankSearch last={
compact index ===
onSelect={(tank) => { antagonist.tank.turrets.length - 1
mutateDuel((draft) => { }
draft.antagonist!.tank = tank; key={turret.id}
draft.antagonist!.engine = tank.engines.at(-1)!; onClick={() => {
draft.antagonist!.track = tank.tracks.at(-1)!; mutateDuel((draft) => {
draft.antagonist!.turret = tank.turrets.at(-1)!; draft.antagonist!.turret = turret;
draft.antagonist!.gun = draft.antagonist!.gun =
draft.antagonist!.turret.guns.at(-1)!; turret.guns.at(-1)!;
draft.antagonist!.shell = draft.antagonist!.shell =
draft.antagonist!.gun.shells[0]; draft.antagonist!.gun.shells[0];
}); });
setAntagonistSelectorVisible(false); mutateTankopediaTemporary((draft) => {
}} draft.shot = undefined;
/> });
</Flex> }}
</Tabs.Content> selected={
antagonist.turret.id === turret.id
<Tabs.Content value="configure"> }
<Flex direction="column" gap="4"> discriminator={
<Flex direction="column" gap="2" style={{ flex: 1 }}> TIER_ROMAN_NUMERALS[turret.tier]
<Heading size="4"> }
{antagonist.tank.name} modules module="turret"
</Heading> />
</Tooltip>
<Flex gap="2" wrap="wrap"> ))}
<Flex> </Flex>
{antagonist.tank.turrets.map( <Flex>
(turret, index) => ( {antagonist.turret.guns.map((gun, index) => (
<Tooltip <Tooltip content={gun.name} key={gun.id}>
content={turret.name} <ModuleButton
key={turret.id} rowChild
> first={index === 0}
<ModuleButton last={
rowChild index ===
first={index === 0} antagonist.turret.guns.length - 1
last={ }
index === onClick={() => {
antagonist.tank.turrets.length - 1 mutateDuel((draft) => {
} draft.antagonist!.gun = gun;
key={turret.id} draft.antagonist!.shell = gun.shells[0];
onClick={() => { });
mutateDuel((draft) => { }}
draft.antagonist!.turret = turret; selected={antagonist.gun.id === gun.id}
draft.antagonist!.gun = discriminator={
turret.guns.at(-1)!; TIER_ROMAN_NUMERALS[gun.tier]
draft.antagonist!.shell = }
draft.antagonist!.gun.shells[0]; module="gun"
}); />
mutateTankopediaTemporary((draft) => { </Tooltip>
draft.shot = undefined; ))}
});
}}
selected={
antagonist.turret.id === turret.id
}
discriminator={
TIER_ROMAN_NUMERALS[turret.tier]
}
module="turret"
/>
</Tooltip>
),
)}
</Flex>
<Flex>
{antagonist.turret.guns.map((gun, index) => (
<Tooltip content={gun.name} key={gun.id}>
<ModuleButton
rowChild
first={index === 0}
last={
index ===
antagonist.turret.guns.length - 1
}
onClick={() => {
mutateDuel((draft) => {
draft.antagonist!.gun = gun;
draft.antagonist!.shell =
gun.shells[0];
});
}}
selected={antagonist.gun.id === gun.id}
discriminator={
TIER_ROMAN_NUMERALS[gun.tier]
}
module="gun"
/>
</Tooltip>
))}
</Flex>
</Flex> </Flex>
</Flex> </Flex>
<Flex direction="column" style={{ flex: 1 }}>
<Heading size="4">Properties</Heading>
<ul>
<li>
Turret: <b>{antagonist.turret.name}</b>
</li>
<li>
Gun: <b>{antagonist.gun.name}</b>
</li>
<li>Shells:</li>
<ul>
{antagonist.gun.shells.map((shell) => (
<li key={shell.id}>
{SHELL_NAMES[shell.type]}:{' '}
<b>
{resolveNearPenetration(
shell.penetration,
)}
mm
</b>
, {shell.damage.armor}HP
</li>
))}
</ul>
</ul>
</Flex>
</Flex> </Flex>
</Tabs.Content>
</Flex> <Flex direction="column" style={{ flex: 1 }}>
</Tabs.Root> <Heading size="4">Properties</Heading>
</Dialog.Content>
</Dialog.Root> <ul>
)} <li>
Turret: <b>{antagonist.turret.name}</b>
</li>
<li>
Gun: <b>{antagonist.gun.name}</b>
</li>
<li>Shells:</li>
<ul>
{antagonist.gun.shells.map((shell) => (
<li key={shell.id}>
{SHELL_NAMES[shell.type]}:{' '}
<b>
{resolveNearPenetration(shell.penetration)}
mm
</b>
, {shell.damage.armor}HP
</li>
))}
</ul>
</ul>
</Flex>
</Flex>
</Tabs.Content>
</Flex>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
</Flex> </Flex>
</Flex> </Flex>
</Card> </Card>
@@ -12,6 +12,7 @@ import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTran
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose'; import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon'; import { tankIcon } from '../../../../../../core/blitzkrieg/tankIcon';
import { useEquipment } from '../../../../../../hooks/useEquipment'; import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
import { useWideFormat } from '../../../../../../hooks/useWideFormat'; import { useWideFormat } from '../../../../../../hooks/useWideFormat';
import { mutateDuel, useDuel } from '../../../../../../stores/duel'; import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import mutateTankopediaPersistent, { import mutateTankopediaPersistent, {
@@ -33,7 +34,6 @@ export function TankSandbox() {
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const canvasWrapper = useRef<HTMLDivElement>(null); const canvasWrapper = useRef<HTMLDivElement>(null);
const [isFullScreen, setIsFullScreen] = useState(false);
const mode = useTankopediaPersistent((state) => state.mode); const mode = useTankopediaPersistent((state) => state.mode);
const protagonist = useDuel((state) => state.protagonist!); const protagonist = useDuel((state) => state.protagonist!);
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
@@ -43,6 +43,7 @@ export function TankSandbox() {
const wideFormat = useWideFormat(); const wideFormat = useWideFormat();
const [loadModel, setLoadModel] = useState(wideFormat || !isMobile); const [loadModel, setLoadModel] = useState(wideFormat || !isMobile);
const duel = useDuel(); const duel = useDuel();
const isFullScreen = useFullScreen();
function handlePointerDown() { function handlePointerDown() {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
@@ -59,10 +60,6 @@ export function TankSandbox() {
} }
useEffect(() => { useEffect(() => {
function handleFullScreenChange() {
setIsFullScreen(document.fullscreenElement !== null);
}
function handlePoseEvent(pose: Pose) { function handlePoseEvent(pose: Pose) {
switch (pose) { switch (pose) {
case Pose.HullDown: { case Pose.HullDown: {
@@ -121,10 +118,8 @@ export function TankSandbox() {
} }
poseEvent.on(handlePoseEvent); poseEvent.on(handlePoseEvent);
document.addEventListener('fullscreenchange', handleFullScreenChange);
return () => { return () => {
document.removeEventListener('fullscreenchange', handleFullScreenChange);
poseEvent.off(handlePoseEvent); poseEvent.off(handlePoseEvent);
}; };
}); });
@@ -145,8 +140,13 @@ export function TankSandbox() {
<Theme radius={isFullScreen ? 'none' : undefined}> <Theme radius={isFullScreen ? 'none' : undefined}>
<Card <Card
style={{ style={{
position: 'relative', position: isFullScreen ? 'fixed' : 'relative',
border: isFullScreen ? 'none' : 'unset', border: isFullScreen ? 'none' : 'unset',
width: isFullScreen ? '100vw' : 'unset',
height: isFullScreen ? 'calc(100vh)' : 'unset',
top: isFullScreen ? 0 : 'unset',
left: isFullScreen ? 0 : 'unset',
zIndex: isFullScreen ? 2 : 'unset',
}} }}
ref={canvasWrapper} ref={canvasWrapper}
> >
@@ -18,7 +18,7 @@ interface OptionsProps {
canvasWrapper: RefObject<HTMLElement>; canvasWrapper: RefObject<HTMLElement>;
} }
export function Options({ isFullScreen, canvasWrapper }: OptionsProps) { export function Options({ isFullScreen }: OptionsProps) {
const showGrid = useTankopediaPersistent( const showGrid = useTankopediaPersistent(
(state) => state.model.visual.showGrid, (state) => state.model.visual.showGrid,
); );
@@ -50,136 +50,132 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
right: 18, right: 18,
}} }}
> >
{!isFullScreen && ( <DropdownMenu.Root>
<DropdownMenu.Root> <DropdownMenu.Trigger>
<DropdownMenu.Trigger> <Button variant="ghost">
<Button variant="ghost"> <EyeOpenIcon />
<EyeOpenIcon /> </Button>
</Button> </DropdownMenu.Trigger>
</DropdownMenu.Trigger>
<DropdownMenu.Content> <DropdownMenu.Content>
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}> <DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}>
Hull down Hull down
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}> <DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}>
Face hug Face hug
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}> <DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}>
Default Default
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
)}
{!isFullScreen && ( <DropdownMenu.Root>
<DropdownMenu.Root> <DropdownMenu.Trigger>
<DropdownMenu.Trigger> <Button variant="ghost">
<Button variant="ghost"> <GearIcon />
<GearIcon /> </Button>
</Button> </DropdownMenu.Trigger>
</DropdownMenu.Trigger>
<DropdownMenu.Content> <DropdownMenu.Content>
<DropdownMenu.Label>Armor</DropdownMenu.Label> <DropdownMenu.Label>Armor</DropdownMenu.Label>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={showSpacedArmor} checked={showSpacedArmor}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showSpacedArmor = checked; draft.model.visual.showSpacedArmor = checked;
}); });
mutateTankopediaTemporary((draft) => { mutateTankopediaTemporary((draft) => {
draft.shot = undefined; draft.shot = undefined;
}); });
}} }}
> >
Spaced armor Spaced armor
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={greenPenetration} checked={greenPenetration}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.greenPenetration = checked; draft.model.visual.greenPenetration = checked;
}); });
}} }}
> >
Green penetration Green penetration
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={wireframe} checked={wireframe}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.wireframe = checked; draft.model.visual.wireframe = checked;
}); });
}} }}
> >
Wireframe Wireframe
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={opaque} checked={opaque}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.opaque = checked; draft.model.visual.opaque = checked;
}); });
}} }}
> >
Opaque Opaque
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.Label>Environment</DropdownMenu.Label> <DropdownMenu.Label>Environment</DropdownMenu.Label>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={showGrid} checked={showGrid}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showGrid = checked; draft.model.visual.showGrid = checked;
}); });
}} }}
> >
Show grid Show grid
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
checked={showEnvironment} checked={showEnvironment}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showEnvironment = checked; draft.model.visual.showEnvironment = checked;
}); });
}} }}
> >
View environment View environment
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
<DropdownMenu.Sub> <DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Lighting</DropdownMenu.SubTrigger> <DropdownMenu.SubTrigger>Lighting</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent> <DropdownMenu.SubContent>
<DropdownMenu.RadioGroup value={environment}> <DropdownMenu.RadioGroup value={environment}>
{ENVIRONMENTS.map((environment) => ( {ENVIRONMENTS.map((environment) => (
<DropdownMenu.RadioItem <DropdownMenu.RadioItem
key={environment} key={environment}
value={environment} value={environment}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.environment = environment; draft.model.visual.environment = environment;
}); });
}} }}
> >
{environment[0].toUpperCase() + environment.slice(1)} {environment[0].toUpperCase() + environment.slice(1)}
</DropdownMenu.RadioItem> </DropdownMenu.RadioItem>
))} ))}
</DropdownMenu.RadioGroup> </DropdownMenu.RadioGroup>
</DropdownMenu.SubContent> </DropdownMenu.SubContent>
</DropdownMenu.Sub> </DropdownMenu.Sub>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
)}
{fullScreenAvailable && ( {fullScreenAvailable && (
<Button <Button
@@ -187,7 +183,7 @@ export function Options({ isFullScreen, canvasWrapper }: OptionsProps) {
onClick={() => { onClick={() => {
if (isFullScreen) { if (isFullScreen) {
document.exitFullscreen(); document.exitFullscreen();
} else canvasWrapper.current?.requestFullscreen(); } else document.body.requestFullscreen();
}} }}
> >
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />} {isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
+5
View File
@@ -1,10 +1,15 @@
import { DiscordLogoIcon } from '@radix-ui/react-icons'; import { DiscordLogoIcon } from '@radix-ui/react-icons';
import { Flex, Text, Theme } from '@radix-ui/themes'; import { Flex, Text, Theme } from '@radix-ui/themes';
import Link from 'next/link'; import Link from 'next/link';
import { useFullScreen } from '../../hooks/useFullScreen';
import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide'; import { BlitzkriegWormWide } from '../../icons/BlitzkriegWormWide';
import { theme } from '../../stitches.config'; import { theme } from '../../stitches.config';
export default function Navbar() { export default function Navbar() {
const isFullScreen = useFullScreen();
if (isFullScreen) return null;
return ( return (
<Theme radius="none"> <Theme radius="none">
<div <div
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
export function useFullScreen() {
const [isFullScreen, setIsFullScreen] = useState(
document.fullscreenElement !== null,
);
useEffect(() => {
function handleFullScreenChange() {
setIsFullScreen(document.fullscreenElement !== null);
}
document.addEventListener('fullscreenchange', handleFullScreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullScreenChange);
};
}, []);
return isFullScreen;
}