Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
391285e5cc | ||
|
|
9f692ccf61 | ||
|
|
47d7f606c6 | ||
|
|
5aa0764844 | ||
|
|
93c55dc9f7 | ||
|
|
aab7441323 | ||
|
|
2ba36b91d5 | ||
|
|
a2726ae287 | ||
|
|
7515561883 |
+22
-4
@@ -9,16 +9,34 @@ import '@fontsource/roboto/700.css';
|
||||
import './css/reset.css';
|
||||
import './css/app.css';
|
||||
import { MobileContext } from './components/infoview/context';
|
||||
import { useWindowDimensions } from './window_width';
|
||||
import { useMobile } from './hooks';
|
||||
import { AUTO_SWITCH_THRESHOLD, getWindowDimensions} from './state/preferences';
|
||||
|
||||
import { connection } from './connection';
|
||||
|
||||
export const GameIdContext = React.createContext<string>(undefined);
|
||||
|
||||
function App() {
|
||||
const { mobile, setMobile, lockMobile, setLockMobile } = useMobile();
|
||||
|
||||
const params = useParams()
|
||||
const gameId = "g/" + params.owner + "/" + params.repo
|
||||
const {width, height} = useWindowDimensions()
|
||||
const [mobile, setMobile] = React.useState(width < 800)
|
||||
|
||||
const automaticallyAdjustLayout = () => {
|
||||
const {width} = getWindowDimensions()
|
||||
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
||||
}
|
||||
|
||||
React.useEffect(()=>{
|
||||
if (!lockMobile){
|
||||
void automaticallyAdjustLayout()
|
||||
window.addEventListener('resize', automaticallyAdjustLayout)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', automaticallyAdjustLayout)
|
||||
}
|
||||
}
|
||||
}, [lockMobile])
|
||||
|
||||
React.useEffect(() => {
|
||||
connection.startLeanClient(gameId);
|
||||
@@ -27,7 +45,7 @@ function App() {
|
||||
return (
|
||||
<div className="app">
|
||||
<GameIdContext.Provider value={gameId}>
|
||||
<MobileContext.Provider value={{mobile, setMobile}}>
|
||||
<MobileContext.Provider value={{mobile, setMobile, lockMobile, setLockMobile}}>
|
||||
<Outlet />
|
||||
</MobileContext.Provider>
|
||||
</GameIdContext.Provider>
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
||||
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||
faCircleInfo, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
||||
faCircleInfo, faTerminal, faMobileScreenButton, faDesktop, faGear } from '@fortawesome/free-solid-svg-icons'
|
||||
import { GameIdContext } from "../app"
|
||||
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
|
||||
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
||||
@@ -150,22 +150,23 @@ function InventoryButton({pageNumber, setPageNumber}) {
|
||||
}
|
||||
|
||||
/** the navigation bar on the welcome page */
|
||||
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo} : {
|
||||
export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpressum, toggleEraseMenu, toggleUploadMenu, toggleInfo, togglePreferencesPopup} : {
|
||||
pageNumber: number,
|
||||
setPageNumber: any,
|
||||
gameInfo: GameInfo,
|
||||
toggleImpressum: any,
|
||||
toggleEraseMenu: any,
|
||||
toggleUploadMenu: any,
|
||||
toggleInfo: any
|
||||
toggleInfo: any,
|
||||
togglePreferencesPopup: () => void;
|
||||
}) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const {mobile, setMobile} = React.useContext(MobileContext)
|
||||
const [navOpen, setNavOpen] = React.useState(false)
|
||||
|
||||
return <div className="app-bar">
|
||||
<div>
|
||||
<div className='app-bar-left'>
|
||||
<Button inverted="false" title="back to games selection" to="/">
|
||||
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faGlobe} />
|
||||
</Button>
|
||||
@@ -194,6 +195,9 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres
|
||||
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
|
||||
<FontAwesomeIcon icon={faCircleInfo} /> Impressum
|
||||
</Button>
|
||||
<Button title="Preferences" inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
|
||||
<FontAwesomeIcon icon={faGear} /> Preferences
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -237,7 +241,7 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
||||
</> :
|
||||
<>
|
||||
{/* DESKTOP VERSION */}
|
||||
<div>
|
||||
<div className='app-bar-left'>
|
||||
<HomeButton isDropdown={false} />
|
||||
<span className="app-bar-title">{worldTitle && `World: ${worldTitle}`}</span>
|
||||
</div>
|
||||
|
||||
@@ -62,12 +62,18 @@ export const ProofStateContext = React.createContext<{
|
||||
setProofState: () => {},
|
||||
})
|
||||
|
||||
export const MobileContext = React.createContext<{
|
||||
export interface IMobileContext {
|
||||
mobile : boolean,
|
||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||
}>({
|
||||
mobile : false,
|
||||
lockMobile: boolean,
|
||||
setLockMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
||||
}
|
||||
|
||||
export const MobileContext = React.createContext<IMobileContext>({
|
||||
mobile: false,
|
||||
setMobile: () => {},
|
||||
lockMobile: false,
|
||||
setLockMobile: () => {}
|
||||
})
|
||||
|
||||
export const WorldLevelIdContext = React.createContext<{
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react'
|
||||
import { Input, Typography } from '@mui/material'
|
||||
import Markdown from '../markdown'
|
||||
import Switch from '@mui/material/Switch';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
||||
import { IMobileContext } from "../infoview/context"
|
||||
|
||||
interface PreferencesPopupProps extends IMobileContext{
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile, handleClose }: PreferencesPopupProps) {
|
||||
return <div className="modal-wrapper">
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<Typography variant="body1" component="div" className="settings">
|
||||
<div className='preferences-category'>
|
||||
<div className='category-title'>
|
||||
<h3>Mobile layout</h3>
|
||||
</div>
|
||||
<div className='preferences-item'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={mobile}
|
||||
onChange={() => setMobile(!mobile)}
|
||||
name="checked"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Enable"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</div>
|
||||
<div className='preferences-item'>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={!lockMobile}
|
||||
onChange={() => setLockMobile(!lockMobile)}
|
||||
name="checked"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Auto"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { InfoPopup } from './popup/game_info'
|
||||
import { PrivacyPolicyPopup } from './popup/privacy_policy'
|
||||
import { RulesHelpPopup } from './popup/rules_help'
|
||||
import { UploadPopup } from './popup/upload'
|
||||
import { PreferencesPopup} from "./popup/preferences"
|
||||
import { WorldTreePanel } from './world_tree'
|
||||
|
||||
import '../css/welcome.css'
|
||||
@@ -63,7 +64,7 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string,
|
||||
/** main page of the game showing among others the tree of worlds/levels */
|
||||
function Welcome() {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const {mobile} = React.useContext(MobileContext)
|
||||
const {mobile, setMobile, lockMobile, setLockMobile} = React.useContext(MobileContext)
|
||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||
|
||||
@@ -77,15 +78,20 @@ function Welcome() {
|
||||
const [info, setInfo] = React.useState(false)
|
||||
const [rulesHelp, setRulesHelp] = React.useState(false)
|
||||
const [uploadMenu, setUploadMenu] = React.useState(false)
|
||||
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
|
||||
|
||||
function closeEraseMenu() {setEraseMenu(false)}
|
||||
function closeImpressum() {setImpressum(false)}
|
||||
function closeInfo() {setInfo(false)}
|
||||
function closeRulesHelp() {setRulesHelp(false)}
|
||||
function closeUploadMenu() {setUploadMenu(false)}
|
||||
function closePreferencesPopup() {setPreferencesPopup(false)}
|
||||
function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
|
||||
function toggleImpressum() {setImpressum(!impressum)}
|
||||
function toggleInfo() {setInfo(!info)}
|
||||
function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
|
||||
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
|
||||
|
||||
|
||||
// set the window title
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ function Welcome() {
|
||||
: <>
|
||||
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
|
||||
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
|
||||
toggleInfo={toggleInfo} />
|
||||
toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
|
||||
<div className="app-content">
|
||||
{ mobile ?
|
||||
<div className="welcome mobile">
|
||||
@@ -128,6 +134,7 @@ function Welcome() {
|
||||
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
|
||||
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
|
||||
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
|
||||
{preferencesPopup ? <PreferencesPopup mobile={mobile} setMobile={setMobile} lockMobile={lockMobile} setLockMobile={setLockMobile} handleClose={closePreferencesPopup}/> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -105,11 +105,18 @@ em {
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.1em;
|
||||
filter: drop-shadow(0 0 5px rgba(0,0,0,0.5));
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.app-bar > .app-bar-left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
}
|
||||
|
||||
.app-bar-title, .app-bar-subtitle {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -317,11 +317,6 @@ td code {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#home-btn {
|
||||
margin-right: .5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.menu.dropdown .svg-inline--fa {
|
||||
width: 1.8rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||
import type { RootState, AppDispatch } from './state/store'
|
||||
|
||||
import { setMobile as setMobileState, setLockMobile as setLockMobileState} from "./state/preferences"
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
|
||||
export const useMobile = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const mobile = useAppSelector((state) => state.preferences.mobile);
|
||||
const lockMobile = useAppSelector((state) => state.preferences.lockMobile);
|
||||
|
||||
const setMobile = (val: boolean) => {
|
||||
dispatch(setMobileState(val));
|
||||
};
|
||||
|
||||
const setLockMobile = (val: boolean) => {
|
||||
dispatch(setLockMobileState(val));
|
||||
};
|
||||
|
||||
return {
|
||||
mobile,
|
||||
setMobile,
|
||||
lockMobile,
|
||||
setLockMobile,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,3 +36,24 @@ export async function saveState(state: any) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
const PREFERENCES_KEY = "preferences"
|
||||
|
||||
/** Load from browser storage */
|
||||
export function loadPreferences() {
|
||||
try {
|
||||
const serializedState = localStorage.getItem(PREFERENCES_KEY);
|
||||
return JSON.parse(serializedState)
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function savePreferences(state: any) {
|
||||
try {
|
||||
const serializedState = JSON.stringify(state)
|
||||
localStorage.setItem(PREFERENCES_KEY, serializedState);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
import { loadPreferences } from "./local_storage";
|
||||
|
||||
interface PreferencesState {
|
||||
mobile: boolean;
|
||||
lockMobile: boolean;
|
||||
}
|
||||
|
||||
export function getWindowDimensions() {
|
||||
const {innerWidth: width, innerHeight: height } = window
|
||||
return {width, height}
|
||||
}
|
||||
|
||||
const { width } = getWindowDimensions()
|
||||
|
||||
export const AUTO_SWITCH_THRESHOLD = 800
|
||||
|
||||
const initialState: PreferencesState = loadPreferences() ?? {
|
||||
mobile: width < AUTO_SWITCH_THRESHOLD,
|
||||
lockMobile: false
|
||||
}
|
||||
|
||||
export const preferencesSlice = createSlice({
|
||||
name: "preferences",
|
||||
initialState,
|
||||
reducers: {
|
||||
setMobile: (state, action) => {
|
||||
state.mobile = action.payload;
|
||||
},
|
||||
setLockMobile: (state, action) => {
|
||||
state.lockMobile = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMobile, setLockMobile } = preferencesSlice.actions;
|
||||
@@ -7,13 +7,15 @@ import { debounce } from "debounce";
|
||||
import { connection } from '../connection'
|
||||
import { apiSlice } from './api'
|
||||
import { progressSlice } from './progress'
|
||||
import { saveState } from "./local_storage";
|
||||
import { preferencesSlice } from "./preferences"
|
||||
import { saveState, savePreferences } from "./local_storage";
|
||||
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[apiSlice.reducerPath]: apiSlice.reducer,
|
||||
[progressSlice.name]: progressSlice.reducer,
|
||||
[preferencesSlice.name]: preferencesSlice.reducer,
|
||||
},
|
||||
// Make connection available in thunks:
|
||||
middleware: getDefaultMiddleware =>
|
||||
@@ -31,6 +33,7 @@ export const store = configureStore({
|
||||
store.subscribe(
|
||||
debounce(() => {
|
||||
saveState(store.getState()[progressSlice.name]);
|
||||
savePreferences(store.getState()[preferencesSlice.name]);
|
||||
}, 800)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
|
||||
function getWindowDimensions() {
|
||||
const {innerWidth: width, innerHeight: height } = window
|
||||
return {width, height}
|
||||
}
|
||||
|
||||
export function useWindowDimensions() {
|
||||
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowDimensions(getWindowDimensions())
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
|
||||
}, [])
|
||||
|
||||
return windowDimensions
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#/bin/bash
|
||||
|
||||
# Note: This fails if there is no default toolchain installed
|
||||
ELAN_HOME=$(lake env printenv ELAN_HOME)
|
||||
|
||||
# $1 : the game directory
|
||||
|
||||
Reference in New Issue
Block a user