Merge pull request #168 from Wzixiao/mobile-option

Mobile option (template)
wasm2
Jon Eugster 1 year ago committed by GitHub
commit d7f1f70d41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,16 +9,33 @@ import '@fontsource/roboto/700.css';
import './css/reset.css'; import './css/reset.css';
import './css/app.css'; import './css/app.css';
import { MobileContext } from './components/infoview/context'; 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'; import { connection } from './connection';
export const GameIdContext = React.createContext<string>(undefined); export const GameIdContext = React.createContext<string>(undefined);
function App() { function App() {
const { mobile, setMobile, lockMobile, setLockMobile } = useMobile();
const params = useParams() const params = useParams()
const gameId = "g/" + params.owner + "/" + params.repo 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(() => { React.useEffect(() => {
connection.startLeanClient(gameId); connection.startLeanClient(gameId);
@ -27,7 +44,7 @@ function App() {
return ( return (
<div className="app"> <div className="app">
<GameIdContext.Provider value={gameId}> <GameIdContext.Provider value={gameId}>
<MobileContext.Provider value={{mobile, setMobile}}> <MobileContext.Provider value={{mobile, setMobile, lockMobile, setLockMobile}}>
<Outlet /> <Outlet />
</MobileContext.Provider> </MobileContext.Provider>
</GameIdContext.Provider> </GameIdContext.Provider>

@ -5,7 +5,7 @@ import * as React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome, import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
faArrowRight, faArrowLeft, faXmark, faBars, faCode, 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 { GameIdContext } from "../app"
import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context" import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context"
import { GameInfo, useGetGameInfoQuery } from '../state/api' import { GameInfo, useGetGameInfoQuery } from '../state/api'
@ -150,18 +150,19 @@ function InventoryButton({pageNumber, setPageNumber}) {
} }
/** the navigation bar on the welcome page */ /** 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, pageNumber: number,
setPageNumber: any, setPageNumber: any,
gameInfo: GameInfo, gameInfo: GameInfo,
toggleImpressum: any, toggleImpressum: any,
toggleEraseMenu: any, toggleEraseMenu: any,
toggleUploadMenu: any, toggleUploadMenu: any,
toggleInfo: any toggleInfo: any,
togglePreferencesPopup: () => void;
}) { }) {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const gameProgress = useAppSelector(selectProgress(gameId)) const gameProgress = useAppSelector(selectProgress(gameId))
const {mobile} = React.useContext(MobileContext) const {mobile, setMobile} = React.useContext(MobileContext)
const [navOpen, setNavOpen] = React.useState(false) const [navOpen, setNavOpen] = React.useState(false)
return <div className="app-bar"> return <div className="app-bar">
@ -194,6 +195,9 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres
<Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}> <Button title="Impressum, privacy policy" inverted="true" to="" onClick={() => {toggleImpressum(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;Impressum <FontAwesomeIcon icon={faCircleInfo} />&nbsp;Impressum
</Button> </Button>
<Button title="Preferences" inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faGear} />&nbsp;Preferences
</Button>
</div> </div>
</div> </div>
} }

@ -62,12 +62,18 @@ export const ProofStateContext = React.createContext<{
setProofState: () => {}, setProofState: () => {},
}) })
export const MobileContext = React.createContext<{ export interface IMobileContext {
mobile : boolean, mobile : boolean,
setMobile: React.Dispatch<React.SetStateAction<Boolean>>, setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
}>({ lockMobile: boolean,
mobile : false, setLockMobile: React.Dispatch<React.SetStateAction<Boolean>>,
}
export const MobileContext = React.createContext<IMobileContext>({
mobile: false,
setMobile: () => {}, setMobile: () => {},
lockMobile: false,
setLockMobile: () => {}
}) })
export const WorldLevelIdContext = React.createContext<{ 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 { PrivacyPolicyPopup } from './popup/privacy_policy'
import { RulesHelpPopup } from './popup/rules_help' import { RulesHelpPopup } from './popup/rules_help'
import { UploadPopup } from './popup/upload' import { UploadPopup } from './popup/upload'
import { PreferencesPopup} from "./popup/preferences"
import { WorldTreePanel } from './world_tree' import { WorldTreePanel } from './world_tree'
import '../css/welcome.css' 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 */ /** main page of the game showing among others the tree of worlds/levels */
function Welcome() { function Welcome() {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {mobile} = React.useContext(MobileContext) const {mobile, setMobile, lockMobile, setLockMobile} = React.useContext(MobileContext)
const gameInfo = useGetGameInfoQuery({game: gameId}) const gameInfo = useGetGameInfoQuery({game: gameId})
const inventory = useLoadInventoryOverviewQuery({game: gameId}) const inventory = useLoadInventoryOverviewQuery({game: gameId})
@ -77,15 +78,20 @@ function Welcome() {
const [info, setInfo] = React.useState(false) const [info, setInfo] = React.useState(false)
const [rulesHelp, setRulesHelp] = React.useState(false) const [rulesHelp, setRulesHelp] = React.useState(false)
const [uploadMenu, setUploadMenu] = React.useState(false) const [uploadMenu, setUploadMenu] = React.useState(false)
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
function closeEraseMenu() {setEraseMenu(false)} function closeEraseMenu() {setEraseMenu(false)}
function closeImpressum() {setImpressum(false)} function closeImpressum() {setImpressum(false)}
function closeInfo() {setInfo(false)} function closeInfo() {setInfo(false)}
function closeRulesHelp() {setRulesHelp(false)} function closeRulesHelp() {setRulesHelp(false)}
function closeUploadMenu() {setUploadMenu(false)} function closeUploadMenu() {setUploadMenu(false)}
function closePreferencesPopup() {setPreferencesPopup(false)}
function toggleEraseMenu() {setEraseMenu(!eraseMenu)} function toggleEraseMenu() {setEraseMenu(!eraseMenu)}
function toggleImpressum() {setImpressum(!impressum)} function toggleImpressum() {setImpressum(!impressum)}
function toggleInfo() {setInfo(!info)} function toggleInfo() {setInfo(!info)}
function toggleUploadMenu() {setUploadMenu(!uploadMenu)} function toggleUploadMenu() {setUploadMenu(!uploadMenu)}
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
// set the window title // set the window title
useEffect(() => { useEffect(() => {
@ -101,7 +107,7 @@ function Welcome() {
: <> : <>
<WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum} <WelcomeAppBar pageNumber={pageNumber} setPageNumber={setPageNumber} gameInfo={gameInfo.data} toggleImpressum={toggleImpressum}
toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu} toggleEraseMenu={toggleEraseMenu} toggleUploadMenu={toggleUploadMenu}
toggleInfo={toggleInfo} /> toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
<div className="app-content"> <div className="app-content">
{ mobile ? { mobile ?
<div className="welcome mobile"> <div className="welcome mobile">
@ -128,6 +134,7 @@ function Welcome() {
{eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null} {eraseMenu? <ErasePopup handleClose={closeEraseMenu}/> : null}
{uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null} {uploadMenu? <UploadPopup handleClose={closeUploadMenu}/> : null}
{info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null} {info ? <InfoPopup info={gameInfo.data?.info} handleClose={closeInfo}/> : null}
{preferencesPopup ? <PreferencesPopup mobile={mobile} setMobile={setMobile} lockMobile={lockMobile} setLockMobile={setLockMobile} handleClose={closePreferencesPopup}/> : null}
</> </>
} }

@ -1,6 +1,30 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './state/store' 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` // Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector 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 // 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 { connection } from '../connection'
import { apiSlice } from './api' import { apiSlice } from './api'
import { progressSlice } from './progress' import { progressSlice } from './progress'
import { saveState } from "./local_storage"; import { preferencesSlice } from "./preferences"
import { saveState, savePreferences } from "./local_storage";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
[apiSlice.reducerPath]: apiSlice.reducer, [apiSlice.reducerPath]: apiSlice.reducer,
[progressSlice.name]: progressSlice.reducer, [progressSlice.name]: progressSlice.reducer,
[preferencesSlice.name]: preferencesSlice.reducer,
}, },
// Make connection available in thunks: // Make connection available in thunks:
middleware: getDefaultMiddleware => middleware: getDefaultMiddleware =>
@ -31,6 +33,7 @@ export const store = configureStore({
store.subscribe( store.subscribe(
debounce(() => { debounce(() => {
saveState(store.getState()[progressSlice.name]); saveState(store.getState()[progressSlice.name]);
savePreferences(store.getState()[preferencesSlice.name]);
}, 800) }, 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
}
Loading…
Cancel
Save