Compare commits

..

2 Commits

Author SHA1 Message Date
Viktor Rådberg
eb99cff736 parse settings before setting 2024-03-29 23:18:10 +01:00
Viktor Rådberg
318520ea53 do 2024-03-29 22:46:55 +01:00
17 changed files with 9020 additions and 9970 deletions

View File

@@ -7,7 +7,7 @@ jobs:
build_and_deploy: build_and_deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
VITE_REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }} REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3

1
.npmrc
View File

@@ -1 +0,0 @@
node-linker=hoisted

1
env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare const APP_VERSION: string;

View File

@@ -1,12 +1,11 @@
{ {
"name": "life-trinket", "name": "life-trinket",
"private": true, "private": true,
"version": "0.9.2", "version": "0.8.1",
"type": "commonjs", "type": "commonjs",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
"yarn": "use pnpm", "npm": "please use bun or yarn :) "
"npm": "please use pnpm"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

9603
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -133,12 +133,10 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
trackMouse: true, trackMouse: true,
onSwipedDown: (e) => { onSwipedDown: (e) => {
e.event.stopPropagation(); e.event.stopPropagation();
analytics.trackEvent('open_player_menu_swipe');
setShowPlayerMenu(true); setShowPlayerMenu(true);
}, },
onSwipedUp: (e) => { onSwipedUp: (e) => {
e.event.stopPropagation(); e.event.stopPropagation();
analytics.trackEvent('close_player_menu_swipe');
setShowPlayerMenu(false); setShowPlayerMenu(false);
}, },
@@ -229,7 +227,6 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
{settings.showPlayerMenuCog && ( {settings.showPlayerMenuCog && (
<SettingsButton <SettingsButton
onClick={() => { onClick={() => {
analytics.trackEvent('open_player_menu_button');
setShowPlayerMenu(!showPlayerMenu); setShowPlayerMenu(!showPlayerMenu);
}} }}
rotation={player.settings.rotation} rotation={player.settings.rotation}

View File

@@ -3,8 +3,6 @@ import { twc } from 'react-twc';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { Cross } from '../../Icons/generated'; import { Cross } from '../../Icons/generated';
import { useEffect } from 'react';
import { useAnalytics } from '../../Hooks/useAnalytics';
export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`; export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`;
@@ -14,17 +12,6 @@ type InfoModalProps = {
}; };
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => { export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
const analytics = useAnalytics();
useEffect(() => {
if (!isOpen) {
return;
}
analytics.trackEvent('info_opened');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
return ( return (
<Modal <Modal
open={isOpen} open={isOpen}

View File

@@ -1,5 +1,5 @@
import { Modal, Switch } from '@mui/material'; import { Button, Modal, Switch } from '@mui/material';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { twc } from 'react-twc'; import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings'; import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { Cross } from '../../Icons/generated'; import { Cross } from '../../Icons/generated';
@@ -7,55 +7,76 @@ import { PreStartMode } from '../../Types/Settings';
import { ModalWrapper } from './InfoModal'; import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { Paragraph } from './TextComponents'; import { Paragraph } from './TextComponents';
import { useAnalytics } from '../../Hooks/useAnalytics';
const SettingContainer = twc.div`w-full flex flex-col mb-2`; const SettingContainer = twc.div`w-full flex flex-col mb-2`;
const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`; const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`;
const Container = twc.div`flex flex-col items-start w-full`; const Container = twc.div`flex flex-col items-center w-full`;
const Description = twc.p`mr-16 text-xs text-left text-text-secondary`; const Description = twc.p`mr-16 text-xs text-left text-text-secondary`;
const baseGithubReleasesUrl =
'https://github.com/Vikeo/LifeTrinket/releases/tag/';
type SettingsModalProps = { type SettingsModalProps = {
isOpen: boolean; isOpen: boolean;
closeModal: () => void; closeModal: () => void;
}; };
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => { export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA, version } = useGlobalSettings(); const { settings, setSettings, isPWA } = useGlobalSettings();
const analytics = useAnalytics(); const [isLatestVersion, setIsLatestVersion] = useState(false);
const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
return; return;
} }
async function checkIfLatestVersion() {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
/* @ts-expect-error is defined in vite.config.ts*/
Authorization: `Bearer ${REPO_READ_ACCESS_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
analytics.trackEvent('settings_opened'); if (!data.name) {
version.checkForNewVersion('settings'); setNewVersion(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps setIsLatestVersion(false);
return;
}
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) {
setIsLatestVersion(true);
return;
}
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
checkIfLatestVersion();
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal <Modal
open={isOpen} open={isOpen}
onClose={() => { onClose={closeModal}
analytics.trackEvent('settings_outside_clicked');
closeModal();
}}
className="w-full flex justify-center" className="w-full flex justify-center"
> >
<> <>
<div className="flex justify-center items-center relative w-full max-w-[532px]"> <div className="flex justify-center items-center relative w-full max-w-[532px]">
<button <button
onClick={() => { onClick={closeModal}
analytics.trackEvent('settings_cross_clicked');
closeModal();
}}
className="flex absolute top-12 right-0 z-10 w-10 h-10 bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2" className="flex absolute top-12 right-0 z-10 w-10 h-10 bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
> >
<Cross size="16px" className="text-text-primary " /> <Cross size="16px" className="text-text-primary " />
@@ -63,71 +84,31 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</div> </div>
<ModalWrapper> <ModalWrapper>
<Container> <Container>
<h2 className="text-center text-2xl mb-2 w-full"> Settings </h2> <h2 className="text-center text-2xl mb-2"> Settings </h2>
<div className="flex flex-col mb-2 w-full"> <SettingContainer>
<div className="text-text-primary flex items-center gap-2"> <Paragraph>
Current version: {version.installedVersion}{' '} {/* @ts-expect-error is defined in vite.config.ts*/}
{version.isLatest && ( Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span> <span className="text-sm text-text-secondary">(latest)</span>
)} )}
<div className="text-xs text-text-primary opacity-75"> </Paragraph>
( {!isLatestVersion && newVersion && (
<a <Paragraph className="text-text-secondary text-lg text-center">
href={baseGithubReleasesUrl + version.installedVersion} New version ({newVersion}) is available!{' '}
target="_blank" </Paragraph>
className="underline"
onClick={() => {
analytics.trackEvent(
`current_change_log_clicked_v${version.installedVersion}`
);
}}
>
Release notes
</a>
)
</div>
</div>
{!version.isLatest && version.remoteVersion && (
<>
<div className="flex gap-2 items-center mt-2">
<Paragraph className="text-text-secondary">
{version.remoteVersion} available!
</Paragraph>
<div className="text-xs text-text-primary opacity-75">
(
<a
href={baseGithubReleasesUrl + version.remoteVersion}
target="_blank"
className="underline"
onClick={() => {
analytics.trackEvent(
`new_change_log_clicked_v${version.remoteVersion}`
);
}}
>
Release notes
</a>
)
</div>
</div>
<button
className="flex justify-center items-center self-start mt-2 bg-primary-main px-3 py-1 rounded-md"
onClick={() => {
{
analytics.trackEvent(`pressed_update`, {
toVersion: version.remoteVersion,
fromVersion: version.installedVersion,
});
window?.location?.reload();
}
}}
>
<span className="text-sm">Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</button>
</>
)} )}
</div> </SettingContainer>
{!isLatestVersion && newVersion && (
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)}
<Separator height="1px" /> <Separator height="1px" />
<SettingContainer> <SettingContainer>
@@ -168,7 +149,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer> </SettingContainer>
<SettingContainer> <SettingContainer>
<div className="flex flex-row justify-between items-center mb-1"> <div className="flex flex-row justify-between items-center mb-1">
<label htmlFor="pre-start-modes">Player selection style</label> <label htmlFor="pre-start-modes">Pre-Start mode</label>
<select <select
name="pre-start-modes" name="pre-start-modes"
id="pre-start-modes" id="pre-start-modes"
@@ -182,11 +163,9 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}} }}
disabled={!settings.showStartingPlayer} disabled={!settings.showStartingPlayer}
> >
<option value={PreStartMode.None}>Instant</option> <option value={PreStartMode.None}>None</option>
<option value={PreStartMode.RandomKing}>Royal Shuffle</option> <option value={PreStartMode.RandomKing}>Random King</option>
<option value={PreStartMode.FingerGame}> <option value={PreStartMode.FingerGame}>Finger Game</option>
Touch Roulette
</option>
</select> </select>
</div> </div>
<div className="text-xs text-left text-text-secondary"> <div className="text-xs text-left text-text-secondary">
@@ -196,13 +175,13 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
{settings.preStartMode === PreStartMode.None && ( {settings.preStartMode === PreStartMode.None && (
<div className="text-xs text-left text-text-secondary mt-1"> <div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Instant:</span> A random <span className="text-text-primary">None:</span> The starting
starting player will simply be shown on start. player will simply be shown.
</div> </div>
)} )}
{settings.preStartMode === PreStartMode.RandomKing && ( {settings.preStartMode === PreStartMode.RandomKing && (
<div className="text-xs text-left text-text-secondary mt-1"> <div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Royal Shuffle:</span>{' '} <span className="text-text-primary">Random King:</span>{' '}
Randomly pass a crown between all players, press the screen to Randomly pass a crown between all players, press the screen to
stop it. The player who has the crown when it stops gets to stop it. The player who has the crown when it stops gets to
start. start.
@@ -210,7 +189,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
)} )}
{settings.preStartMode === PreStartMode.FingerGame && ( {settings.preStartMode === PreStartMode.FingerGame && (
<div className="text-xs text-left text-text-secondary mt-1"> <div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Touch Roulette:</span> All <span className="text-text-primary">Finger Game:</span> All
players put a finger on the screen, one will be chosen at players put a finger on the screen, one will be chosen at
random. random.
</div> </div>
@@ -255,16 +234,6 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
enabled. enabled.
</Description> </Description>
</SettingContainer> </SettingContainer>
<Separator height="1px" />
<button
className="flex justify-center self-center items-center mt-1 mb-1 bg-primary-main px-3 py-1 rounded-md"
onClick={() => {
analytics.trackEvent('settings_save_clicked');
closeModal();
}}
>
<span className="text-sm">Save and Close</span>
</button>
{!isPWA && ( {!isPWA && (
<> <>
<Separator height="1px" /> <Separator height="1px" />
@@ -285,6 +254,14 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</> </>
)} )}
<Separator height="1px" /> <Separator height="1px" />
<Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close
</Button>
</Container> </Container>
</ModalWrapper> </ModalWrapper>
</> </>

View File

@@ -18,7 +18,6 @@ import {
} from '../../Icons/generated'; } from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player'; import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage'; import { RotationDivProps } from '../Buttons/CommanderDamage';
import { useAnalytics } from '../../Hooks/useAnalytics';
const PlayerMenuWrapper = twc.div` const PlayerMenuWrapper = twc.div`
flex flex
@@ -112,8 +111,6 @@ const PlayerMenu = ({
setRandomizingPlayer, setRandomizingPlayer,
} = useGlobalSettings(); } = useGlobalSettings();
const analytics = useAnalytics();
const { updatePlayer, resetCurrentGame } = usePlayers(); const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -133,7 +130,6 @@ const PlayerMenu = ({
setShowPlayerMenu(false); setShowPlayerMenu(false);
setPlaying(false); setPlaying(false);
setRandomizingPlayer(true); setRandomizingPlayer(true);
analytics.trackEvent('reset_game');
}; };
const handleGoToStart = () => { const handleGoToStart = () => {
@@ -177,10 +173,7 @@ const PlayerMenu = ({
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<button <button
onClick={() => { onClick={() => setShowPlayerMenu(false)}
analytics.trackEvent('close_player_menu_button');
setShowPlayerMenu(false);
}}
className="flex absolute top-0 right-2 z-10 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2 p-[0.2rem]" className="flex absolute top-0 right-2 z-10 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2 p-[0.2rem]"
> >
<Cross size={buttonFontSize} className="text-primary-main " /> <Cross size={buttonFontSize} className="text-primary-main " />
@@ -194,11 +187,6 @@ const PlayerMenu = ({
type="color" type="color"
className="size-[200%] absolute -left-2 -top-2" className="size-[200%] absolute -left-2 -top-2"
value={player.color} value={player.color}
onClick={() => {
analytics.trackEvent('color_picker_opened', {
player: player.index,
});
}}
/> />
</ColorPickerButton> </ColorPickerButton>
{player.settings.useCommanderDamage && ( {player.settings.useCommanderDamage && (
@@ -222,12 +210,7 @@ const PlayerMenu = ({
strokeWidth="30" strokeWidth="30"
/> />
} }
onChange={(e) => { onChange={handleSettingsChange}
analytics.trackEvent('toggle_partner', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox" role="checkbox"
aria-checked={player.settings.usePartner} aria-checked={player.settings.usePartner}
aria-label="Partner" aria-label="Partner"
@@ -254,12 +237,7 @@ const PlayerMenu = ({
strokeWidth="30" strokeWidth="30"
/> />
} }
onChange={(e) => { onChange={handleSettingsChange}
analytics.trackEvent('toggle_poison', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox" role="checkbox"
aria-checked={player.settings.usePoison} aria-checked={player.settings.usePoison}
aria-label="Poison" aria-label="Poison"
@@ -285,12 +263,7 @@ const PlayerMenu = ({
strokeWidth="15" strokeWidth="15"
/> />
} }
onChange={(e) => { onChange={handleSettingsChange}
analytics.trackEvent('toggle_energy', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox" role="checkbox"
aria-checked={player.settings.useEnergy} aria-checked={player.settings.useEnergy}
aria-label="Energy" aria-label="Energy"
@@ -316,12 +289,7 @@ const PlayerMenu = ({
strokeWidth="15" strokeWidth="15"
/> />
} }
onChange={(e) => { onChange={handleSettingsChange}
analytics.trackEvent('toggle_experience', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox" role="checkbox"
aria-checked={player.settings.useExperience} aria-checked={player.settings.useExperience}
aria-label="Experience" aria-label="Experience"

View File

@@ -9,10 +9,10 @@ import { useGlobalSettings } from '../../../Hooks/useGlobalSettings';
import { usePlayers } from '../../../Hooks/usePlayers'; import { usePlayers } from '../../../Hooks/usePlayers';
import { Cog, Info } from '../../../Icons/generated'; import { Cog, Info } from '../../../Icons/generated';
import { import {
GameFormat,
InitialGameSettings, InitialGameSettings,
Orientation, Orientation,
PreStartMode, PreStartMode,
defaultInitialGameSettings,
} from '../../../Types/Settings'; } from '../../../Types/Settings';
import { InfoModal } from '../../Misc/InfoModal'; import { InfoModal } from '../../Misc/InfoModal';
import { SettingsModal } from '../../Misc/SettingsModal'; import { SettingsModal } from '../../Misc/SettingsModal';
@@ -91,38 +91,20 @@ const Start = () => {
settings, settings,
isPWA, isPWA,
setRandomizingPlayer, setRandomizingPlayer,
version,
} = useGlobalSettings(); } = useGlobalSettings();
const [openInfoModal, setOpenInfoModal] = useState(false); const [openInfoModal, setOpenInfoModal] = useState(false);
const [openSettingsModal, setOpenSettingsModal] = useState(false); const [openSettingsModal, setOpenSettingsModal] = useState(false);
const [playerOptions, setPlayerOptions] = useState<InitialGameSettings>( const [playerOptions, setPlayerOptions] = useState<InitialGameSettings>(
initialGameSettings || defaultInitialGameSettings initialGameSettings || {
); numberOfPlayers: 4,
startingLifeTotal: 40,
let tracked = false; useCommanderDamage: true,
// Check for new version on mount orientation: Orientation.Portrait,
useEffect(() => { gameFormat: GameFormat.Commander,
if (!tracked) {
console.log('checking version');
version.checkForNewVersion('start_menu');
// eslint-disable-next-line react-hooks/exhaustive-deps
tracked = true;
} }
}, []); );
useEffect(() => {
setInitialGameSettings(playerOptions);
}, [playerOptions, setInitialGameSettings]);
useEffect(() => {
setPlayerOptions({
...playerOptions,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerOptions.numberOfPlayers]);
const doStartGame = () => { const doStartGame = () => {
if (!initialGameSettings) { if (!initialGameSettings) {
@@ -151,10 +133,21 @@ const Start = () => {
localStorage.setItem('showPlay', 'true'); localStorage.setItem('showPlay', 'true');
}; };
const valueText = (value: number) => { useEffect(() => {
setInitialGameSettings(playerOptions);
}, [playerOptions, setInitialGameSettings]);
const valuetext = (value: number) => {
return `${value}`; return `${value}`;
}; };
useEffect(() => {
setPlayerOptions({
...playerOptions,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerOptions.numberOfPlayers]);
return ( return (
<MainWrapper> <MainWrapper>
<Info <Info
@@ -182,12 +175,8 @@ const Start = () => {
<SupportMe /> <SupportMe />
<h1 className="relative flex flex-col text-3xl font-bold mt-6 mb-6 text-text-primary justify-center items-center"> <h1 className="text-3xl block font-bold mt-6 mb-5 text-text-primary">
Life Trinket Life Trinket
<div className="h-[1px] w-[120%] bg-common-white opacity-50" />
<div className="flex absolute text-xs font-medium -bottom-4">
v{version.installedVersion}
</div>
</h1> </h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4"> <div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
@@ -200,7 +189,7 @@ const Start = () => {
min={1} min={1}
aria-label="Custom marks" aria-label="Custom marks"
value={playerOptions?.numberOfPlayers ?? 4} value={playerOptions?.numberOfPlayers ?? 4}
getAriaValueText={valueText} getAriaValueText={valuetext}
step={null} step={null}
marks={playerMarks} marks={playerMarks}
onChange={(_e, value) => { onChange={(_e, value) => {
@@ -221,7 +210,7 @@ const Start = () => {
min={20} min={20}
aria-label="Custom marks" aria-label="Custom marks"
value={playerOptions?.startingLifeTotal ?? 40} value={playerOptions?.startingLifeTotal ?? 40}
getAriaValueText={valueText} getAriaValueText={valuetext}
step={10} step={10}
marks={healthMarks} marks={healthMarks}
onChange={(_e, value) => onChange={(_e, value) =>
@@ -264,32 +253,15 @@ const Start = () => {
}} }}
/> />
</ToggleContainer> </ToggleContainer>
<div className="flex flex-nowrap text-nowrap relative justify-center items-start"> <Button
<Button variant="contained"
variant="contained" style={{ height: '2rem' }}
style={{ height: '2rem' }} onClick={() => {
onClick={() => { setOpenSettingsModal(true);
setOpenSettingsModal(true); }}
}} >
> <Cog /> &nbsp; Other settings
<Cog /> &nbsp; Game Settings </Button>
</Button>
<div
data-not-latest-version={
!version.isLatest && !!version.remoteVersion
}
className="absolute flex justify-center text-text-primary text-xxs -bottom-5 bg-primary-dark px-2 rounded-md
opacity-0 transition-all duration-200 delay-500
data-[not-latest-version=true]:opacity-100
"
>
<div className="absolute bg-primary-dark rotate-45 size-2 -top-[2px] z-0" />
<span className="z-10">
v{version.remoteVersion} available!
</span>
</div>
</div>
</ToggleButtonsWrapper> </ToggleButtonsWrapper>
<FormLabel>Layout</FormLabel> <FormLabel>Layout</FormLabel>

View File

@@ -1,13 +1,6 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { InitialGameSettings, Settings } from '../Types/Settings'; import { InitialGameSettings, Settings } from '../Types/Settings';
type Version = {
installedVersion: string;
isLatest: boolean;
checkForNewVersion: (source: 'settings' | 'start_menu') => Promise<void>;
remoteVersion?: string;
};
export type GlobalSettingsContextType = { export type GlobalSettingsContextType = {
fullscreen: { fullscreen: {
isFullscreen: boolean; isFullscreen: boolean;
@@ -36,8 +29,6 @@ export type GlobalSettingsContextType = {
isPWA: boolean; isPWA: boolean;
preStartCompleted: boolean; preStartCompleted: boolean;
setPreStartCompleted: (completed: boolean) => void; setPreStartCompleted: (completed: boolean) => void;
version: Version;
}; };
export const GlobalSettingsContext = export const GlobalSettingsContext =

View File

@@ -7,10 +7,9 @@ import {
import { useAnalytics } from '../Hooks/useAnalytics'; import { useAnalytics } from '../Hooks/useAnalytics';
import { import {
InitialGameSettings, InitialGameSettings,
Settings,
defaultInitialGameSettings,
defaultSettings,
initialGameSettingsSchema, initialGameSettingsSchema,
PreStartMode,
Settings,
settingsSchema, settingsSchema,
} from '../Types/Settings'; } from '../Types/Settings';
@@ -54,18 +53,19 @@ export const GlobalSettingsProvider = ({
savedGameSettings ? JSON.parse(savedGameSettings) : null savedGameSettings ? JSON.parse(savedGameSettings) : null
); );
const setInitialGameSettingsAndLocalStorage = ( const parsedSettings = settingsSchema.safeParse(
initialGameSettings: InitialGameSettings JSON.parse(savedSettings ?? '')
) => { );
setInitialGameSettings(initialGameSettings);
localStorage.setItem(
'initialGameSettings',
JSON.stringify(initialGameSettings)
);
};
const [settings, setSettings] = useState<Settings>( const [settings, setSettings] = useState<Settings>(
savedSettings ? JSON.parse(savedSettings) : defaultSettings parsedSettings.success
? parsedSettings.data
: {
goFullscreenOnStart: true,
keepAwake: true,
showStartingPlayer: true,
showPlayerMenuCog: true,
preStartMode: PreStartMode.None,
}
); );
const setSettingsAndLocalStorage = (settings: Settings) => { const setSettingsAndLocalStorage = (settings: Settings) => {
@@ -85,29 +85,10 @@ export const GlobalSettingsProvider = ({
setPreStartCompleted(false); setPreStartCompleted(false);
}; };
// Set settings if they are not valid
useEffect(() => { useEffect(() => {
// If there are no saved settings, set default settings if (savedGameSettings && JSON.parse(savedGameSettings).gridAreas) {
if (!savedSettings) { removeLocalStorage();
setSettingsAndLocalStorage(defaultSettings); window.location.reload();
return;
}
const parsedSettings = settingsSchema.safeParse(JSON.parse(savedSettings));
// If saved settings are not valid, remove them
if (!parsedSettings.success) {
console.error('invalid settings, resetting to default settings');
setSettingsAndLocalStorage(defaultSettings);
return;
}
localStorage.setItem('settings', JSON.stringify(parsedSettings.data));
}, [settings, savedSettings]);
// Set initial game settings if they are not valid
useEffect(() => {
if (!savedGameSettings) {
setInitialGameSettingsAndLocalStorage(defaultInitialGameSettings);
return; return;
} }
@@ -116,14 +97,14 @@ export const GlobalSettingsProvider = ({
initialGameSettingsSchema.safeParse(initialGameSettings); initialGameSettingsSchema.safeParse(initialGameSettings);
if (!parsedInitialGameSettings.success) { if (!parsedInitialGameSettings.success) {
console.error('invalid game settings, resetting to default settings'); removeLocalStorage();
setInitialGameSettingsAndLocalStorage(defaultInitialGameSettings); window.location.reload();
return; return;
} }
localStorage.setItem( localStorage.setItem(
'initialGameSettings', 'initialGameSettings',
JSON.stringify(parsedInitialGameSettings.data) JSON.stringify(initialGameSettings)
); );
}, [initialGameSettings, savedGameSettings]); }, [initialGameSettings, savedGameSettings]);
@@ -142,11 +123,6 @@ export const GlobalSettingsProvider = ({
}; };
}, []); }, []);
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [remoteVersion, setRemoteVersion] = useState<string | undefined>(
undefined
);
const { isSupported, release, released, request, type } = useWakeLock(); const { isSupported, release, released, request, type } = useWakeLock();
const active = settings.keepAwake; const active = settings.keepAwake;
@@ -200,46 +176,6 @@ export const GlobalSettingsProvider = ({
localStorage.setItem('playing', String(playing)); localStorage.setItem('playing', String(playing));
}; };
async function checkForNewVersion(source: 'settings' | 'start_menu') {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
Authorization: `Bearer ${
import.meta.env.VITE_REPO_READ_ACCESS_TOKEN
}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setRemoteVersion(undefined);
setIsLatestVersion(false);
return;
}
setRemoteVersion(data.name);
if (data.name === import.meta.env.VITE_APP_VERSION) {
setIsLatestVersion(true);
return;
}
analytics.trackEvent(`${source}_has_new_version`, {
remoteVersion: data.name,
installedVersion: import.meta.env.VITE_APP_VERSION,
});
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
return { return {
fullscreen: { isFullscreen, enableFullscreen, disableFullscreen }, fullscreen: { isFullscreen, enableFullscreen, disableFullscreen },
wakeLock: { wakeLock: {
@@ -264,13 +200,6 @@ export const GlobalSettingsProvider = ({
isPWA: window?.matchMedia('(display-mode: standalone)').matches, isPWA: window?.matchMedia('(display-mode: standalone)').matches,
preStartCompleted, preStartCompleted,
setPreStartCompleted: setPreStartCompletedAndLocalStorage, setPreStartCompleted: setPreStartCompletedAndLocalStorage,
version: {
installedVersion: import.meta.env.VITE_APP_VERSION,
remoteVersion,
isLatest: isLatestVersion,
checkForNewVersion,
},
}; };
}, [ }, [
isFullscreen, isFullscreen,
@@ -285,8 +214,6 @@ export const GlobalSettingsProvider = ({
settings, settings,
randomizingPlayer, randomizingPlayer,
preStartCompleted, preStartCompleted,
remoteVersion,
isLatestVersion,
analytics, analytics,
]); ]);

View File

@@ -35,33 +35,17 @@ export type InitialGameSettings = {
}; };
export const initialGameSettingsSchema = z.object({ export const initialGameSettingsSchema = z.object({
startingLifeTotal: z.number().min(1).max(200), startingLifeTotal: z.number().min(1).max(200).default(20),
useCommanderDamage: z.boolean(), useCommanderDamage: z.boolean().default(false),
gameFormat: z.nativeEnum(GameFormat), gameFormat: z.nativeEnum(GameFormat).optional(),
numberOfPlayers: z.number().min(1).max(6), numberOfPlayers: z.number().min(1).max(6).default(2),
orientation: z.nativeEnum(Orientation), orientation: z.nativeEnum(Orientation).default(Orientation.Landscape),
}); });
export const defaultInitialGameSettings = {
numberOfPlayers: 4,
startingLifeTotal: 40,
useCommanderDamage: true,
orientation: Orientation.Landscape,
gameFormat: GameFormat.Commander,
};
export const settingsSchema = z.object({ export const settingsSchema = z.object({
keepAwake: z.boolean(), keepAwake: z.boolean().default(true),
showStartingPlayer: z.boolean(), showStartingPlayer: z.boolean().default(true),
showPlayerMenuCog: z.boolean(), showPlayerMenuCog: z.boolean().default(true),
goFullscreenOnStart: z.boolean(), goFullscreenOnStart: z.boolean().default(true),
preStartMode: z.nativeEnum(PreStartMode), preStartMode: z.nativeEnum(PreStartMode).default(PreStartMode.None),
}); });
export const defaultSettings: Settings = {
goFullscreenOnStart: true,
keepAwake: true,
showStartingPlayer: true,
showPlayerMenuCog: true,
preStartMode: PreStartMode.None,
};

9
src/vite-env.d.ts vendored
View File

@@ -1,10 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_VERSION: string;
readonly VITE_REPO_READ_ACCESS_TOKEN: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -103,9 +103,6 @@ export default {
animation: { animation: {
fadeOut: 'fadeOut 3s 1s ease-out forwards', fadeOut: 'fadeOut 3s 1s ease-out forwards',
}, },
fontSize: {
xxs: ['0.625rem', '1rem'],
},
}, },
}, },
plugins: [tailwindcssGridAreas], plugins: [tailwindcssGridAreas],

View File

@@ -21,11 +21,7 @@ export default defineConfig({
}, },
}, },
define: { define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify( APP_VERSION: JSON.stringify(process.env.npm_package_version),
process.env.npm_package_version REPO_READ_ACCESS_TOKEN: JSON.stringify(process.env.REPO_READ_ACCESS_TOKEN),
),
VITE_REPO_READ_ACCESS_TOKEN: JSON.stringify(
process.env.VITE_REPO_READ_ACCESS_TOKEN
),
}, },
}); });

8867
yarn.lock Normal file

File diff suppressed because it is too large Load Diff