Clearer way to see if there is an update, more fun tracking

This commit is contained in:
Viktor Rådberg
2024-03-30 14:04:12 +01:00
parent 28c2ff536f
commit 3276dc81fc
16 changed files with 9903 additions and 8982 deletions

View File

@@ -7,7 +7,7 @@ jobs:
build_and_deploy: build_and_deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }} VITE_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 Normal file
View File

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

1
env.d.ts vendored
View File

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

View File

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

9603
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -133,10 +133,12 @@ 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);
}, },
@@ -227,6 +229,7 @@ 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,6 +3,8 @@ 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]`;
@@ -12,6 +14,17 @@ 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 { Button, Modal, Switch } from '@mui/material'; import { Modal, Switch } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect } 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,76 +7,55 @@ 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-center w-full`; const Container = twc.div`flex flex-col items-start 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 } = useGlobalSettings(); const { settings, setSettings, isPWA, version } = useGlobalSettings();
const [isLatestVersion, setIsLatestVersion] = useState(false); const analytics = useAnalytics();
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();
if (!data.name) { analytics.trackEvent('settings_opened');
setNewVersion(undefined); version.checkForNewVersion('settings');
setIsLatestVersion(false); // eslint-disable-next-line react-hooks/exhaustive-deps
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={closeModal} onClose={() => {
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={closeModal} onClick={() => {
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 " />
@@ -84,31 +63,71 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</div> </div>
<ModalWrapper> <ModalWrapper>
<Container> <Container>
<h2 className="text-center text-2xl mb-2"> Settings </h2> <h2 className="text-center text-2xl mb-2 w-full"> Settings </h2>
<SettingContainer> <div className="flex flex-col mb-2 w-full">
<Paragraph> <div className="text-text-primary flex items-center gap-2">
{/* @ts-expect-error is defined in vite.config.ts*/} Current version: {version.installedVersion}{' '}
Current version: {APP_VERSION}{' '} {version.isLatest && (
{isLatestVersion && (
<span className="text-sm text-text-secondary">(latest)</span> <span className="text-sm text-text-secondary">(latest)</span>
)} )}
</Paragraph> <div className="text-xs text-text-primary opacity-75">
{!isLatestVersion && newVersion && ( (
<Paragraph className="text-text-secondary text-lg text-center"> <a
New version ({newVersion}) is available!{' '} href={baseGithubReleasesUrl + version.installedVersion}
</Paragraph> target="_blank"
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>
</>
)} )}
</SettingContainer> </div>
{!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>
@@ -149,7 +168,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">Pre-Start mode</label> <label htmlFor="pre-start-modes">Player selection style</label>
<select <select
name="pre-start-modes" name="pre-start-modes"
id="pre-start-modes" id="pre-start-modes"
@@ -163,9 +182,11 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}} }}
disabled={!settings.showStartingPlayer} disabled={!settings.showStartingPlayer}
> >
<option value={PreStartMode.None}>None</option> <option value={PreStartMode.None}>Instant</option>
<option value={PreStartMode.RandomKing}>Random King</option> <option value={PreStartMode.RandomKing}>Royal Shuffle</option>
<option value={PreStartMode.FingerGame}>Finger Game</option> <option value={PreStartMode.FingerGame}>
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">
@@ -175,13 +196,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">None:</span> The starting <span className="text-text-primary">Instant:</span> A random
player will simply be shown. starting player will simply be shown on start.
</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">Random King:</span>{' '} <span className="text-text-primary">Royal Shuffle:</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.
@@ -189,7 +210,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">Finger Game:</span> All <span className="text-text-primary">Touch Roulette:</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>
@@ -234,6 +255,16 @@ 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" />
@@ -254,14 +285,6 @@ 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,6 +18,7 @@ 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
@@ -111,6 +112,8 @@ 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>) => {
@@ -130,6 +133,7 @@ const PlayerMenu = ({
setShowPlayerMenu(false); setShowPlayerMenu(false);
setPlaying(false); setPlaying(false);
setRandomizingPlayer(true); setRandomizingPlayer(true);
analytics.trackEvent('reset_game');
}; };
const handleGoToStart = () => { const handleGoToStart = () => {
@@ -173,7 +177,10 @@ const PlayerMenu = ({
ref={settingsContainerRef} ref={settingsContainerRef}
> >
<button <button
onClick={() => setShowPlayerMenu(false)} onClick={() => {
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 " />
@@ -187,6 +194,11 @@ 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 && (
@@ -210,7 +222,12 @@ const PlayerMenu = ({
strokeWidth="30" strokeWidth="30"
/> />
} }
onChange={handleSettingsChange} onChange={(e) => {
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"
@@ -237,7 +254,12 @@ const PlayerMenu = ({
strokeWidth="30" strokeWidth="30"
/> />
} }
onChange={handleSettingsChange} onChange={(e) => {
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"
@@ -263,7 +285,12 @@ const PlayerMenu = ({
strokeWidth="15" strokeWidth="15"
/> />
} }
onChange={handleSettingsChange} onChange={(e) => {
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"
@@ -289,7 +316,12 @@ const PlayerMenu = ({
strokeWidth="15" strokeWidth="15"
/> />
} }
onChange={handleSettingsChange} onChange={(e) => {
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

@@ -91,6 +91,7 @@ const Start = () => {
settings, settings,
isPWA, isPWA,
setRandomizingPlayer, setRandomizingPlayer,
version,
} = useGlobalSettings(); } = useGlobalSettings();
const [openInfoModal, setOpenInfoModal] = useState(false); const [openInfoModal, setOpenInfoModal] = useState(false);
@@ -100,6 +101,29 @@ const Start = () => {
initialGameSettings || defaultInitialGameSettings initialGameSettings || defaultInitialGameSettings
); );
let tracked = false;
// Check for new version on mount
useEffect(() => {
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) {
return; return;
@@ -127,21 +151,10 @@ const Start = () => {
localStorage.setItem('showPlay', 'true'); localStorage.setItem('showPlay', 'true');
}; };
useEffect(() => { const valueText = (value: number) => {
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
@@ -169,8 +182,12 @@ const Start = () => {
<SupportMe /> <SupportMe />
<h1 className="text-3xl block font-bold mt-6 mb-5 text-text-primary"> <h1 className="relative flex flex-col text-3xl font-bold mt-6 mb-6 text-text-primary justify-center items-center">
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">
@@ -183,7 +200,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) => {
@@ -204,7 +221,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) =>
@@ -247,15 +264,32 @@ const Start = () => {
}} }}
/> />
</ToggleContainer> </ToggleContainer>
<Button <div className="flex flex-nowrap text-nowrap relative justify-center items-start">
variant="contained" <Button
style={{ height: '2rem' }} variant="contained"
onClick={() => { style={{ height: '2rem' }}
setOpenSettingsModal(true); onClick={() => {
}} setOpenSettingsModal(true);
> }}
<Cog /> &nbsp; Other settings >
</Button> <Cog /> &nbsp; Game Settings
</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,6 +1,13 @@
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;
@@ -29,6 +36,8 @@ 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

@@ -142,6 +142,11 @@ 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;
@@ -195,6 +200,46 @@ 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: {
@@ -219,6 +264,13 @@ 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,
@@ -233,6 +285,8 @@ export const GlobalSettingsProvider = ({
settings, settings,
randomizingPlayer, randomizingPlayer,
preStartCompleted, preStartCompleted,
remoteVersion,
isLatestVersion,
analytics, analytics,
]); ]);

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

@@ -1 +1,10 @@
/// <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,6 +103,9 @@ 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,7 +21,11 @@ export default defineConfig({
}, },
}, },
define: { define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version), 'import.meta.env.VITE_APP_VERSION': JSON.stringify(
REPO_READ_ACCESS_TOKEN: JSON.stringify(process.env.REPO_READ_ACCESS_TOKEN), process.env.npm_package_version
),
VITE_REPO_READ_ACCESS_TOKEN: JSON.stringify(
process.env.VITE_REPO_READ_ACCESS_TOKEN
),
}, },
}); });

8867
yarn.lock

File diff suppressed because it is too large Load Diff