Merge pull request #181 from Wzixiao/auto-mobile-layout
Modify logic for preferences
This commit is contained in:
+6
-22
@@ -8,40 +8,24 @@ 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 { PreferencesContext} from './components/infoview/context';
|
||||||
import { useMobile } from './hooks';
|
import UsePreferences from "./state/hooks/use_preferences"
|
||||||
import { AUTO_SWITCH_THRESHOLD, getWindowDimensions} from './state/preferences';
|
|
||||||
|
|
||||||
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 automaticallyAdjustLayout = () => {
|
const {mobile, layout, isSavePreferences, setLayout, setIsSavePreferences} = UsePreferences()
|
||||||
const {width} = getWindowDimensions()
|
|
||||||
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(()=>{
|
|
||||||
if (!lockMobile){
|
|
||||||
void automaticallyAdjustLayout()
|
|
||||||
window.addEventListener('resize', automaticallyAdjustLayout)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', automaticallyAdjustLayout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [lockMobile])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<GameIdContext.Provider value={gameId}>
|
<GameIdContext.Provider value={gameId}>
|
||||||
<MobileContext.Provider value={{mobile, setMobile, lockMobile, setLockMobile}}>
|
<PreferencesContext.Provider value={{mobile, layout, isSavePreferences, setLayout, setIsSavePreferences}}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</MobileContext.Provider>
|
</PreferencesContext.Provider>
|
||||||
</GameIdContext.Provider>
|
</GameIdContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome,
|
|||||||
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
faArrowRight, faArrowLeft, faXmark, faBars, faCode,
|
||||||
faCircleInfo, faTerminal, faMobileScreenButton, faDesktop, faGear } 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, PreferencesContext, WorldLevelIdContext } from "./infoview/context"
|
||||||
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
import { GameInfo, useGetGameInfoQuery } from '../state/api'
|
||||||
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
||||||
@@ -162,7 +162,7 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres
|
|||||||
}) {
|
}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const gameProgress = useAppSelector(selectProgress(gameId))
|
const gameProgress = useAppSelector(selectProgress(gameId))
|
||||||
const {mobile, setMobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
|
|
||||||
return <div className="app-bar">
|
return <div className="app-bar">
|
||||||
@@ -212,7 +212,7 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=
|
|||||||
}) {
|
}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const [navOpen, setNavOpen] = React.useState(false)
|
const [navOpen, setNavOpen] = React.useState(false)
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as React from 'react';
|
|||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
||||||
import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api';
|
import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api';
|
||||||
import { GameHint, InteractiveGoal, InteractiveGoals } from './rpc_api';
|
import { GameHint, InteractiveGoal, InteractiveGoals } from './rpc_api';
|
||||||
|
import { PreferencesState } from '../../state/preferences';
|
||||||
|
|
||||||
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
|
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
|
||||||
null as any)
|
null as any)
|
||||||
@@ -62,18 +63,18 @@ export const ProofStateContext = React.createContext<{
|
|||||||
setProofState: () => {},
|
setProofState: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface IMobileContext {
|
export interface IPreferencesContext extends PreferencesState{
|
||||||
mobile : boolean,
|
mobile: boolean, // The variables that actually control the page 'layout' can only be changed through layout.
|
||||||
setMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
setLayout: React.Dispatch<React.SetStateAction<PreferencesState["layout"]>>;
|
||||||
lockMobile: boolean,
|
setIsSavePreferences: React.Dispatch<React.SetStateAction<Boolean>>;
|
||||||
setLockMobile: React.Dispatch<React.SetStateAction<Boolean>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MobileContext = React.createContext<IMobileContext>({
|
export const PreferencesContext = React.createContext<IPreferencesContext>({
|
||||||
mobile: false,
|
mobile: false,
|
||||||
setMobile: () => {},
|
layout: "auto",
|
||||||
lockMobile: false,
|
isSavePreferences: false,
|
||||||
setLockMobile: () => {}
|
setLayout: () => {},
|
||||||
|
setIsSavePreferences: () => {}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorldLevelIdContext = React.createContext<{
|
export const WorldLevelIdContext = React.createContext<{
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import Markdown from '../markdown';
|
|||||||
import { Infos } from './infos';
|
import { Infos } from './infos';
|
||||||
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
|
||||||
import { Goal } from './goals';
|
import { Goal } from './goals';
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './context';
|
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './context';
|
||||||
import { Typewriter, hasErrors, hasInteractiveErrors } from './typewriter';
|
import { Typewriter, hasErrors, hasInteractiveErrors } from './typewriter';
|
||||||
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
@@ -349,7 +349,7 @@ export function TypewriterInterface({props}) {
|
|||||||
const [disableInput, setDisableInput] = React.useState<boolean>(false)
|
const [disableInput, setDisableInput] = React.useState<boolean>(false)
|
||||||
const [loadingProgress, setLoadingProgress] = React.useState<number>(0)
|
const [loadingProgress, setLoadingProgress] = React.useState<number>(0)
|
||||||
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const { proof } = React.useContext(ProofContext)
|
const { proof } = React.useContext(ProofContext)
|
||||||
const { setTypewriterInput } = React.useContext(InputModeContext)
|
const { setTypewriterInput } = React.useContext(InputModeContext)
|
||||||
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { Button } from './button'
|
|||||||
import Markdown from './markdown'
|
import Markdown from './markdown'
|
||||||
import {InventoryPanel} from './inventory'
|
import {InventoryPanel} from './inventory'
|
||||||
import { hasInteractiveErrors } from './infoview/typewriter'
|
import { hasInteractiveErrors } from './infoview/typewriter'
|
||||||
import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext,
|
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
|
||||||
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
|
||||||
import { DualEditor } from './infoview/main'
|
import { DualEditor } from './infoview/main'
|
||||||
import { GameHint } from './infoview/rpc_api'
|
import { GameHint } from './infoview/rpc_api'
|
||||||
@@ -74,7 +74,7 @@ function Level() {
|
|||||||
|
|
||||||
function ChatPanel({lastLevel}) {
|
function ChatPanel({lastLevel}) {
|
||||||
const chatRef = useRef<HTMLDivElement>(null)
|
const chatRef = useRef<HTMLDivElement>(null)
|
||||||
const {mobile} = useContext(MobileContext)
|
const {mobile} = useContext(PreferencesContext)
|
||||||
const gameId = useContext(GameIdContext)
|
const gameId = useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
||||||
@@ -215,7 +215,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
const codeviewRef = useRef<HTMLDivElement>(null)
|
const codeviewRef = useRef<HTMLDivElement>(null)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ function PlayableLevel({impressum, setImpressum}) {
|
|||||||
function IntroductionPanel({gameInfo}) {
|
function IntroductionPanel({gameInfo}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId} = useContext(WorldLevelIdContext)
|
const {worldId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
let text: Array<string> = gameInfo.data?.worlds.nodes[worldId].introduction.split(/\n(\s*\n)+/)
|
let text: Array<string> = gameInfo.data?.worlds.nodes[worldId].introduction.split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ export default Level
|
|||||||
/** The site with the introduction text of a world */
|
/** The site with the introduction text of a world */
|
||||||
function Introduction({impressum, setImpressum}) {
|
function Introduction({impressum, setImpressum}) {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {mobile} = useContext(MobileContext)
|
const {mobile} = useContext(PreferencesContext)
|
||||||
|
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,42 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Input, Typography } from '@mui/material'
|
import { Input, Typography } from '@mui/material'
|
||||||
import Markdown from '../markdown'
|
import Markdown from '../markdown'
|
||||||
import Switch from '@mui/material/Switch';
|
import { Switch, Button, ButtonGroup } from '@mui/material';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Slider from '@mui/material/Slider';
|
||||||
|
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
|
||||||
import { IMobileContext } from "../infoview/context"
|
import { IPreferencesContext } from "../infoview/context"
|
||||||
|
|
||||||
interface PreferencesPopupProps extends IMobileContext{
|
interface PreferencesPopupProps extends Omit<IPreferencesContext, 'mobile'> {
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PreferencesPopup({ layout, setLayout, isSavePreferences, setIsSavePreferences, handleClose }: PreferencesPopupProps) {
|
||||||
|
|
||||||
|
const marks = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: 'Mobile',
|
||||||
|
key: "mobile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: 'Auto',
|
||||||
|
key: "auto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: 'Desktop',
|
||||||
|
key: "desktop"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handlerChangeLayout = (_: Event, value: number) => {
|
||||||
|
setLayout(marks[value].key as IPreferencesContext["layout"])
|
||||||
|
}
|
||||||
|
|
||||||
export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile, handleClose }: PreferencesPopupProps) {
|
|
||||||
return <div className="modal-wrapper">
|
return <div className="modal-wrapper">
|
||||||
<div className="modal-backdrop" onClick={handleClose} />
|
<div className="modal-backdrop" onClick={handleClose} />
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
@@ -18,34 +44,43 @@ export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile,
|
|||||||
<Typography variant="body1" component="div" className="settings">
|
<Typography variant="body1" component="div" className="settings">
|
||||||
<div className='preferences-category'>
|
<div className='preferences-category'>
|
||||||
<div className='category-title'>
|
<div className='category-title'>
|
||||||
<h3>Mobile layout</h3>
|
<h3>Layout</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className='preferences-item'>
|
<div className='preferences-item first leave-left-gap'>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Box sx={{ width: 300 }}>
|
||||||
checked={mobile}
|
<Slider
|
||||||
onChange={() => setMobile(!mobile)}
|
aria-label="Always visible"
|
||||||
name="checked"
|
value={marks.find(item => item.key === layout).value}
|
||||||
color="primary"
|
step={1}
|
||||||
/>
|
marks={marks}
|
||||||
|
max={2}
|
||||||
|
sx={{
|
||||||
|
'& .MuiSlider-track': { display: 'none', },
|
||||||
|
}}
|
||||||
|
onChange={handlerChangeLayout}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
label="Enable"
|
label=""
|
||||||
labelPlacement="start"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='preferences-category tail-category'>
|
||||||
<div className='preferences-item'>
|
<div className='preferences-item'>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
checked={!lockMobile}
|
checked={isSavePreferences}
|
||||||
onChange={() => setLockMobile(!lockMobile)}
|
onChange={() => setIsSavePreferences(!isSavePreferences)}
|
||||||
name="checked"
|
name="checked"
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Auto"
|
label="Save my settings (in the browser store)"
|
||||||
labelPlacement="start"
|
labelPlacement="end"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from '../hooks'
|
|||||||
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
|
||||||
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api'
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { MobileContext } from './infoview/context'
|
import { PreferencesContext } from './infoview/context'
|
||||||
import { InventoryPanel } from './inventory'
|
import { InventoryPanel } from './inventory'
|
||||||
import { ErasePopup } from './popup/erase'
|
import { ErasePopup } from './popup/erase'
|
||||||
import { InfoPopup } from './popup/game_info'
|
import { InfoPopup } from './popup/game_info'
|
||||||
@@ -27,7 +27,7 @@ import { Hint } from './hints'
|
|||||||
|
|
||||||
/** the panel showing the game's introduction text */
|
/** the panel showing the game's introduction text */
|
||||||
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
||||||
const {mobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -64,7 +64,9 @@ 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, setMobile, lockMobile, setLockMobile} = React.useContext(MobileContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
const {layout, isSavePreferences, setLayout, setIsSavePreferences} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
||||||
|
|
||||||
@@ -134,7 +136,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}
|
{preferencesPopup ? <PreferencesPopup layout={layout} isSavePreferences={isSavePreferences} setLayout={setLayout} setIsSavePreferences={setIsSavePreferences} handleClose={closePreferencesPopup}/> : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { GameIdContext } from '../app'
|
import { GameIdContext } from '../app'
|
||||||
import { useAppDispatch, useMobile } from '../hooks'
|
import { useAppDispatch } from '../hooks'
|
||||||
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress'
|
||||||
import { store } from '../state/store'
|
import { store } from '../state/store'
|
||||||
|
|
||||||
import '../css/world_tree.css'
|
import '../css/world_tree.css'
|
||||||
|
import { PreferencesContext } from './infoview/context'
|
||||||
|
|
||||||
// Settings for the world tree
|
// Settings for the world tree
|
||||||
cytoscape.use( klay )
|
cytoscape.use( klay )
|
||||||
@@ -197,7 +198,7 @@ export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
|
|||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const difficulty = useSelector(selectDifficulty(gameId))
|
const difficulty = useSelector(selectDifficulty(gameId))
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { mobile } = useMobile()
|
const { mobile } = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
|
|
||||||
function label(x : number) {
|
function label(x : number) {
|
||||||
|
|||||||
@@ -187,3 +187,15 @@ h5, h6 {
|
|||||||
margin-left: 0.3rem;
|
margin-left: 0.3rem;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preferences-category.tail-category{
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-item.first{
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-item.leave-left-gap{
|
||||||
|
margin-left: 3em;
|
||||||
|
}
|
||||||
@@ -1,30 +1,6 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../hooks";
|
||||||
|
import {
|
||||||
|
PreferencesState,
|
||||||
|
setLayout as setPreferencesLayout,
|
||||||
|
setIsSavePreferences as setPreferencesIsSavePreferences,
|
||||||
|
getWindowDimensions,
|
||||||
|
AUTO_SWITCH_THRESHOLD
|
||||||
|
} from "../preferences";
|
||||||
|
|
||||||
|
|
||||||
|
const UsePreferences = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const [mobile, setMobile] = React.useState<boolean>()
|
||||||
|
|
||||||
|
const layout = useAppSelector((state) => state.preferences.layout);
|
||||||
|
const setLayout = (layout: PreferencesState["layout"]) => dispatch(setPreferencesLayout(layout))
|
||||||
|
|
||||||
|
const isSavePreferences = useAppSelector((state) => state.preferences.isSavePreferences);
|
||||||
|
const setIsSavePreferences = (isSave: boolean) => dispatch(setPreferencesIsSavePreferences(isSave))
|
||||||
|
|
||||||
|
const automaticallyAdjustLayout = () => {
|
||||||
|
const {width} = getWindowDimensions()
|
||||||
|
setMobile(width < AUTO_SWITCH_THRESHOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(()=>{
|
||||||
|
if (layout === "auto"){
|
||||||
|
void automaticallyAdjustLayout()
|
||||||
|
window.addEventListener('resize', automaticallyAdjustLayout)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', automaticallyAdjustLayout)
|
||||||
|
} else {
|
||||||
|
setMobile(layout === "mobile")
|
||||||
|
}
|
||||||
|
}, [layout])
|
||||||
|
|
||||||
|
return {mobile, layout, isSavePreferences, setLayout, setIsSavePreferences}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsePreferences;
|
||||||
@@ -57,3 +57,12 @@ export function savePreferences(state: any) {
|
|||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removePreferences() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(PREFERENCES_KEY);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
import { loadPreferences } from "./local_storage";
|
import { loadPreferences, removePreferences, savePreferences } from "./local_storage";
|
||||||
|
|
||||||
interface PreferencesState {
|
export interface PreferencesState {
|
||||||
mobile: boolean;
|
layout: "mobile" | "auto" | "desktop";
|
||||||
lockMobile: boolean;
|
isSavePreferences: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWindowDimensions() {
|
export function getWindowDimensions() {
|
||||||
@@ -12,26 +12,24 @@ export function getWindowDimensions() {
|
|||||||
return {width, height}
|
return {width, height}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { width } = getWindowDimensions()
|
|
||||||
|
|
||||||
export const AUTO_SWITCH_THRESHOLD = 800
|
export const AUTO_SWITCH_THRESHOLD = 800
|
||||||
|
|
||||||
const initialState: PreferencesState = loadPreferences() ?? {
|
const initialState: PreferencesState = loadPreferences() ??{
|
||||||
mobile: width < AUTO_SWITCH_THRESHOLD,
|
layout: "auto",
|
||||||
lockMobile: false
|
isSavePreferences: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const preferencesSlice = createSlice({
|
export const preferencesSlice = createSlice({
|
||||||
name: "preferences",
|
name: "preferences",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setMobile: (state, action) => {
|
setLayout: (state, action) => {
|
||||||
state.mobile = action.payload;
|
state.layout = action.payload;
|
||||||
},
|
},
|
||||||
setLockMobile: (state, action) => {
|
setIsSavePreferences: (state, action) => {
|
||||||
state.lockMobile = action.payload;
|
state.isSavePreferences = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setMobile, setLockMobile } = preferencesSlice.actions;
|
export const { setLayout, setIsSavePreferences } = preferencesSlice.actions;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { connection } from '../connection'
|
|||||||
import { apiSlice } from './api'
|
import { apiSlice } from './api'
|
||||||
import { progressSlice } from './progress'
|
import { progressSlice } from './progress'
|
||||||
import { preferencesSlice } from "./preferences"
|
import { preferencesSlice } from "./preferences"
|
||||||
import { saveState, savePreferences } from "./local_storage";
|
import { saveState, savePreferences, removePreferences} from "./local_storage";
|
||||||
|
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
@@ -29,7 +29,9 @@ 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]);
|
|
||||||
|
const preferencesState = store.getState()[preferencesSlice.name]
|
||||||
|
preferencesState.isSavePreferences ? savePreferences(preferencesState) : removePreferences()
|
||||||
}, 800)
|
}, 800)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user