From 8c93b3c5b3be04b43d32c52301e673d27a384cc9 Mon Sep 17 00:00:00 2001 From: ran Date: Fri, 12 Jan 2024 23:29:03 +0800 Subject: [PATCH 1/7] Modify logic for all preferences --- client/src/app.tsx | 31 ++++++++----- client/src/components/infoview/context.ts | 17 +++++-- client/src/components/popup/preferences.tsx | 51 +++++++++++---------- client/src/components/welcome.tsx | 8 ++-- client/src/components/world_tree.tsx | 5 +- client/src/css/welcome.css | 12 +++++ client/src/hooks.ts | 24 ---------- client/src/state/local_storage.ts | 9 ++++ client/src/state/preferences.ts | 27 ++++++----- client/src/state/store.ts | 6 ++- 10 files changed, 107 insertions(+), 83 deletions(-) diff --git a/client/src/app.tsx b/client/src/app.tsx index 871c226..d5b8647 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -8,39 +8,48 @@ import '@fontsource/roboto/700.css'; import './css/reset.css'; import './css/app.css'; -import { MobileContext } from './components/infoview/context'; -import { useMobile } from './hooks'; -import { AUTO_SWITCH_THRESHOLD, getWindowDimensions} from './state/preferences'; +import { MobileContext, PreferencesContext} from './components/infoview/context'; +import { AUTO_SWITCH_THRESHOLD, getWindowDimensions, setLayout, setisSavePreferences, PreferencesState} from './state/preferences'; +import { useAppDispatch, useAppSelector } from './hooks'; export const GameIdContext = React.createContext(undefined); function App() { - const { mobile, setMobile, lockMobile, setLockMobile } = useMobile(); + const dispatch = useAppDispatch() const params = useParams() const gameId = "g/" + params.owner + "/" + params.repo + // TODO: + const [mobile, setMobile] = React.useState() + const layout = useAppSelector((state) => state.preferences.layout); + const changeLayout = (layout: PreferencesState["layout"]) => dispatch(setLayout(layout)) + const isSavePreferences = useAppSelector((state) => state.preferences.isSavePreferences); + const changeIsSavePreferences = (isSave: boolean) => dispatch(setisSavePreferences(isSave)) + const automaticallyAdjustLayout = () => { const {width} = getWindowDimensions() setMobile(width < AUTO_SWITCH_THRESHOLD) } React.useEffect(()=>{ - if (!lockMobile){ + if (layout === "auto"){ void automaticallyAdjustLayout() window.addEventListener('resize', automaticallyAdjustLayout) - return () => { - window.removeEventListener('resize', automaticallyAdjustLayout) - } + return () => window.removeEventListener('resize', automaticallyAdjustLayout) + } else { + setMobile(layout === "mobile") } - }, [lockMobile]) + }, [layout]) return (
- - + + + +
diff --git a/client/src/components/infoview/context.ts b/client/src/components/infoview/context.ts index 8a62b36..948473b 100644 --- a/client/src/components/infoview/context.ts +++ b/client/src/components/infoview/context.ts @@ -5,6 +5,7 @@ import * as React from 'react'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api'; import { GameHint, InteractiveGoal, InteractiveGoals } from './rpc_api'; +import { PreferencesState } from '../../state/preferences'; export const MonacoEditorContext = React.createContext( null as any) @@ -62,18 +63,26 @@ export const ProofStateContext = React.createContext<{ setProofState: () => {}, }) +export interface IPreferencesContext extends PreferencesState{ + setLayout: React.Dispatch>; + setIsSavePreferences: React.Dispatch>; +} + +export const PreferencesContext = React.createContext({ + layout: "auto", + isSavePreferences: false, + setLayout: () => {}, + setIsSavePreferences: () => {} +}) + export interface IMobileContext { mobile : boolean, setMobile: React.Dispatch>, - lockMobile: boolean, - setLockMobile: React.Dispatch>, } export const MobileContext = React.createContext({ mobile: false, setMobile: () => {}, - lockMobile: false, - setLockMobile: () => {} }) export const WorldLevelIdContext = React.createContext<{ diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx index 73b2dbe..fdda8e6 100644 --- a/client/src/components/popup/preferences.tsx +++ b/client/src/components/popup/preferences.tsx @@ -1,16 +1,20 @@ import * as React from 'react' import { Input, Typography } from '@mui/material' import Markdown from '../markdown' -import Switch from '@mui/material/Switch'; +import { Switch, Button, ButtonGroup } from '@mui/material'; + import FormControlLabel from '@mui/material/FormControlLabel'; import { IMobileContext } from "../infoview/context" +import { PreferencesState } from "../../state/preferences" -interface PreferencesPopupProps extends IMobileContext{ - handleClose: () => void -} +interface PreferencesPopupProps extends PreferencesState{ + handleClose: () => void, + setLayout: (layout: "mobile" | "auto" | "desktop") => void, + setIsSavePreferences: (isSave: boolean) => void +} -export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile, handleClose }: PreferencesPopupProps) { +export function PreferencesPopup({ layout, setLayout, isSavePreferences, setIsSavePreferences, handleClose }: PreferencesPopupProps) { return
@@ -18,34 +22,35 @@ export function PreferencesPopup({ mobile, setMobile, lockMobile, setLockMobile,
-

Mobile layout

+

Layout

-
+
setMobile(!mobile)} - name="checked" - color="primary" - /> + + + + + } - label="Enable" - labelPlacement="start" + label="" />
+
+ +
setLockMobile(!lockMobile)} - name="checked" - color="primary" - /> + setIsSavePreferences(!isSavePreferences)} + name="checked" + color="primary" + /> } - label="Auto" - labelPlacement="start" + label="Save my settings (in the browser store)" + labelPlacement="end" />
diff --git a/client/src/components/welcome.tsx b/client/src/components/welcome.tsx index 89b9adc..18f9f9d 100644 --- a/client/src/components/welcome.tsx +++ b/client/src/components/welcome.tsx @@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from '../hooks' import { changedOpenedIntro, selectOpenedIntro } from '../state/progress' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api' import { Button } from './button' -import { MobileContext } from './infoview/context' +import { MobileContext, PreferencesContext } from './infoview/context' import { InventoryPanel } from './inventory' import { ErasePopup } from './popup/erase' import { InfoPopup } from './popup/game_info' @@ -64,7 +64,9 @@ 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, setMobile, lockMobile, setLockMobile} = React.useContext(MobileContext) + const {mobile, setMobile} = React.useContext(MobileContext) + const {layout, isSavePreferences, setLayout, setIsSavePreferences} = React.useContext(PreferencesContext) + const gameInfo = useGetGameInfoQuery({game: gameId}) const inventory = useLoadInventoryOverviewQuery({game: gameId}) @@ -134,7 +136,7 @@ function Welcome() { {eraseMenu? : null} {uploadMenu? : null} {info ? : null} - {preferencesPopup ? : null} + {preferencesPopup ? : null} } diff --git a/client/src/components/world_tree.tsx b/client/src/components/world_tree.tsx index 1aa495b..76cf5fb 100644 --- a/client/src/components/world_tree.tsx +++ b/client/src/components/world_tree.tsx @@ -11,11 +11,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faXmark, faCircleQuestion } from '@fortawesome/free-solid-svg-icons' import { GameIdContext } from '../app' -import { useAppDispatch, useMobile } from '../hooks' +import { useAppDispatch } from '../hooks' import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/progress' import { store } from '../state/store' import '../css/world_tree.css' +import { MobileContext } from './infoview/context' // Settings for the world tree cytoscape.use( klay ) @@ -197,7 +198,7 @@ export function WorldSelectionMenu({rulesHelp, setRulesHelp}) { const gameId = React.useContext(GameIdContext) const difficulty = useSelector(selectDifficulty(gameId)) const dispatch = useAppDispatch() - const { mobile } = useMobile() + const { mobile } = React.useContext(MobileContext) function label(x : number) { diff --git a/client/src/css/welcome.css b/client/src/css/welcome.css index 94e17e3..387bc48 100644 --- a/client/src/css/welcome.css +++ b/client/src/css/welcome.css @@ -187,3 +187,15 @@ h5, h6 { margin-left: 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: 1em; +} \ No newline at end of file diff --git a/client/src/hooks.ts b/client/src/hooks.ts index 0ac6e2f..afdce0a 100644 --- a/client/src/hooks.ts +++ b/client/src/hooks.ts @@ -1,30 +1,6 @@ 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 = 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, - }; -}; diff --git a/client/src/state/local_storage.ts b/client/src/state/local_storage.ts index ea165ce..098768e 100644 --- a/client/src/state/local_storage.ts +++ b/client/src/state/local_storage.ts @@ -57,3 +57,12 @@ export function savePreferences(state: any) { // Ignore } } + +export function removePreferences() { + try { + localStorage.removeItem(PREFERENCES_KEY); + } catch (e) { + // Ignore + } +} + diff --git a/client/src/state/preferences.ts b/client/src/state/preferences.ts index 6ee0fa1..b00dd71 100644 --- a/client/src/state/preferences.ts +++ b/client/src/state/preferences.ts @@ -1,10 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; -import { loadPreferences } from "./local_storage"; +import { loadPreferences, removePreferences, savePreferences } from "./local_storage"; -interface PreferencesState { - mobile: boolean; - lockMobile: boolean; +export interface PreferencesState { + layout: "mobile" | "auto" | "desktop"; + isSavePreferences: boolean; } export function getWindowDimensions() { @@ -12,26 +12,25 @@ export function getWindowDimensions() { return {width, height} } -const { width } = getWindowDimensions() - export const AUTO_SWITCH_THRESHOLD = 800 -const initialState: PreferencesState = loadPreferences() ?? { - mobile: width < AUTO_SWITCH_THRESHOLD, - lockMobile: false +const initialState: PreferencesState = loadPreferences() ??{ + layout: "auto", + isSavePreferences: false } export const preferencesSlice = createSlice({ name: "preferences", initialState, reducers: { - setMobile: (state, action) => { - state.mobile = action.payload; + setLayout: (state, action) => { + state.layout = action.payload; }, - setLockMobile: (state, action) => { - state.lockMobile = action.payload; + setisSavePreferences: (state, action) => { + state.isSavePreferences = action.payload; + action.payload ? savePreferences(state) : removePreferences() }, }, }); -export const { setMobile, setLockMobile } = preferencesSlice.actions; +export const { setLayout, setisSavePreferences } = preferencesSlice.actions; diff --git a/client/src/state/store.ts b/client/src/state/store.ts index 4406c84..cdda9c5 100644 --- a/client/src/state/store.ts +++ b/client/src/state/store.ts @@ -8,7 +8,7 @@ import { connection } from '../connection' import { apiSlice } from './api' import { progressSlice } from './progress' import { preferencesSlice } from "./preferences" -import { saveState, savePreferences } from "./local_storage"; +import { saveState, savePreferences, removePreferences} from "./local_storage"; export const store = configureStore({ @@ -29,7 +29,9 @@ export const store = configureStore({ store.subscribe( debounce(() => { saveState(store.getState()[progressSlice.name]); - savePreferences(store.getState()[preferencesSlice.name]); + + const preferencesState= store.getState()[preferencesSlice.name] + preferencesState.isSavePreferences ? savePreferences(preferencesState) : removePreferences() }, 800) ); From e15e5af1262947c96f26592c876e2f724f0e8e82 Mon Sep 17 00:00:00 2001 From: ran Date: Fri, 12 Jan 2024 23:31:22 +0800 Subject: [PATCH 2/7] Add todo about setMobile --- client/src/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app.tsx b/client/src/app.tsx index d5b8647..653286f 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -20,7 +20,7 @@ function App() { const params = useParams() const gameId = "g/" + params.owner + "/" + params.repo - // TODO: + // TODO: Modifying setMobile will not change 'layout', and the setMobile function may not exist in the future. const [mobile, setMobile] = React.useState() const layout = useAppSelector((state) => state.preferences.layout); const changeLayout = (layout: PreferencesState["layout"]) => dispatch(setLayout(layout)) From d16956da9b581e2a06733ba864c463c1c492c68c Mon Sep 17 00:00:00 2001 From: ran Date: Fri, 12 Jan 2024 23:43:21 +0800 Subject: [PATCH 3/7] Modify the props definition of the preference popup component --- client/src/components/popup/preferences.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx index fdda8e6..ee24d6c 100644 --- a/client/src/components/popup/preferences.tsx +++ b/client/src/components/popup/preferences.tsx @@ -5,13 +5,10 @@ import { Switch, Button, ButtonGroup } from '@mui/material'; import FormControlLabel from '@mui/material/FormControlLabel'; -import { IMobileContext } from "../infoview/context" -import { PreferencesState } from "../../state/preferences" +import { IPreferencesContext } from "../infoview/context" -interface PreferencesPopupProps extends PreferencesState{ - handleClose: () => void, - setLayout: (layout: "mobile" | "auto" | "desktop") => void, - setIsSavePreferences: (isSave: boolean) => void +interface PreferencesPopupProps extends IPreferencesContext{ + handleClose: () => void } export function PreferencesPopup({ layout, setLayout, isSavePreferences, setIsSavePreferences, handleClose }: PreferencesPopupProps) { From 5d88cd67393331af02c69e61d1c9d501f723bc72 Mon Sep 17 00:00:00 2001 From: ran Date: Fri, 12 Jan 2024 23:44:43 +0800 Subject: [PATCH 4/7] Remove logically duplicated code --- client/src/state/preferences.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/state/preferences.ts b/client/src/state/preferences.ts index b00dd71..f3c3fbb 100644 --- a/client/src/state/preferences.ts +++ b/client/src/state/preferences.ts @@ -28,7 +28,6 @@ export const preferencesSlice = createSlice({ }, setisSavePreferences: (state, action) => { state.isSavePreferences = action.payload; - action.payload ? savePreferences(state) : removePreferences() }, }, }); From 8929813e488af177152c3ce0183b2fff598ae8a5 Mon Sep 17 00:00:00 2001 From: ran Date: Fri, 12 Jan 2024 23:48:47 +0800 Subject: [PATCH 5/7] Added a missing space regarding code style in store.ts --- client/src/state/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/state/store.ts b/client/src/state/store.ts index cdda9c5..e73818e 100644 --- a/client/src/state/store.ts +++ b/client/src/state/store.ts @@ -30,7 +30,7 @@ store.subscribe( debounce(() => { saveState(store.getState()[progressSlice.name]); - const preferencesState= store.getState()[preferencesSlice.name] + const preferencesState = store.getState()[preferencesSlice.name] preferencesState.isSavePreferences ? savePreferences(preferencesState) : removePreferences() }, 800) ); From ab98eaa3ba0263fff43aec1df7cae4d6b9f7f7cb Mon Sep 17 00:00:00 2001 From: ran Date: Tue, 16 Jan 2024 11:21:46 +0800 Subject: [PATCH 6/7] Change the selected button to a slider --- client/src/components/popup/preferences.tsx | 47 ++++++++++++++++++--- client/src/css/welcome.css | 2 +- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx index ee24d6c..2b6d29b 100644 --- a/client/src/components/popup/preferences.tsx +++ b/client/src/components/popup/preferences.tsx @@ -2,16 +2,41 @@ import * as React from 'react' import { Input, Typography } from '@mui/material' import Markdown from '../markdown' 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 { IPreferencesContext } from "../infoview/context" -interface PreferencesPopupProps extends IPreferencesContext{ +interface PreferencesPopupProps extends IPreferencesContext { handleClose: () => void } -export function PreferencesPopup({ layout, setLayout, isSavePreferences, setIsSavePreferences, handleClose }: PreferencesPopupProps) { +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"]) + } + return
@@ -24,11 +49,19 @@ export function PreferencesPopup({ layout, setLayout, isSavePreferences, setIsSa
- - - - + + item.key === layout).value} + step={1} + marks={marks} + max={2} + sx={{ + '& .MuiSlider-track': { display: 'none', }, + }} + onChange={handlerChangeLayout} + /> + } label="" /> diff --git a/client/src/css/welcome.css b/client/src/css/welcome.css index 387bc48..e48f7d0 100644 --- a/client/src/css/welcome.css +++ b/client/src/css/welcome.css @@ -197,5 +197,5 @@ h5, h6 { } .preferences-item.leave-left-gap{ - margin-left: 1em; + margin-left: 3em; } \ No newline at end of file From 9d4a6df139455e297a4205eb97958563017614c2 Mon Sep 17 00:00:00 2001 From: ran Date: Tue, 23 Jan 2024 22:59:43 +0800 Subject: [PATCH 7/7] Remove MobileContext and use PreferencesContext instead --- client/src/app.tsx | 33 ++--------------- client/src/components/app_bar.tsx | 6 +-- client/src/components/infoview/context.ts | 12 +----- client/src/components/infoview/main.tsx | 4 +- client/src/components/level.tsx | 10 ++--- client/src/components/popup/preferences.tsx | 2 +- client/src/components/welcome.tsx | 6 +-- client/src/components/world_tree.tsx | 4 +- client/src/state/hooks/use_preferences.ts | 41 +++++++++++++++++++++ client/src/state/preferences.ts | 4 +- 10 files changed, 65 insertions(+), 57 deletions(-) create mode 100644 client/src/state/hooks/use_preferences.ts diff --git a/client/src/app.tsx b/client/src/app.tsx index 653286f..64e5004 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -8,49 +8,24 @@ import '@fontsource/roboto/700.css'; import './css/reset.css'; import './css/app.css'; -import { MobileContext, PreferencesContext} from './components/infoview/context'; -import { AUTO_SWITCH_THRESHOLD, getWindowDimensions, setLayout, setisSavePreferences, PreferencesState} from './state/preferences'; -import { useAppDispatch, useAppSelector } from './hooks'; +import { PreferencesContext} from './components/infoview/context'; +import UsePreferences from "./state/hooks/use_preferences" export const GameIdContext = React.createContext(undefined); function App() { - const dispatch = useAppDispatch() const params = useParams() const gameId = "g/" + params.owner + "/" + params.repo - // TODO: Modifying setMobile will not change 'layout', and the setMobile function may not exist in the future. - const [mobile, setMobile] = React.useState() - const layout = useAppSelector((state) => state.preferences.layout); - const changeLayout = (layout: PreferencesState["layout"]) => dispatch(setLayout(layout)) - const isSavePreferences = useAppSelector((state) => state.preferences.isSavePreferences); - const changeIsSavePreferences = (isSave: boolean) => dispatch(setisSavePreferences(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]) + const {mobile, layout, isSavePreferences, setLayout, setIsSavePreferences} = UsePreferences() return (
- - + -
) diff --git a/client/src/components/app_bar.tsx b/client/src/components/app_bar.tsx index ced97d5..f8a171f 100644 --- a/client/src/components/app_bar.tsx +++ b/client/src/components/app_bar.tsx @@ -7,7 +7,7 @@ import { faDownload, faUpload, faEraser, faBook, faBookOpen, faGlobe, faHome, faArrowRight, faArrowLeft, faXmark, faBars, faCode, faCircleInfo, faTerminal, faMobileScreenButton, faDesktop, faGear } from '@fortawesome/free-solid-svg-icons' 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 { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress } from '../state/progress' import { useAppDispatch, useAppSelector } from '../hooks' @@ -162,7 +162,7 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres }) { const gameId = React.useContext(GameIdContext) const gameProgress = useAppSelector(selectProgress(gameId)) - const {mobile, setMobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) const [navOpen, setNavOpen] = React.useState(false) return
@@ -212,7 +212,7 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= }) { const gameId = React.useContext(GameIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext) - const {mobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) const [navOpen, setNavOpen] = React.useState(false) const gameInfo = useGetGameInfoQuery({game: gameId}) const completed = useAppSelector(selectCompleted(gameId, worldId, levelId)) diff --git a/client/src/components/infoview/context.ts b/client/src/components/infoview/context.ts index 948473b..e425749 100644 --- a/client/src/components/infoview/context.ts +++ b/client/src/components/infoview/context.ts @@ -64,27 +64,19 @@ export const ProofStateContext = React.createContext<{ }) export interface IPreferencesContext extends PreferencesState{ + mobile: boolean, // The variables that actually control the page 'layout' can only be changed through layout. setLayout: React.Dispatch>; setIsSavePreferences: React.Dispatch>; } export const PreferencesContext = React.createContext({ + mobile: false, layout: "auto", isSavePreferences: false, setLayout: () => {}, setIsSavePreferences: () => {} }) -export interface IMobileContext { - mobile : boolean, - setMobile: React.Dispatch>, -} - -export const MobileContext = React.createContext({ - mobile: false, - setMobile: () => {}, -}) - export const WorldLevelIdContext = React.createContext<{ worldId : string, levelId: number diff --git a/client/src/components/infoview/main.tsx b/client/src/components/infoview/main.tsx index c90eba8..956d2f6 100644 --- a/client/src/components/infoview/main.tsx +++ b/client/src/components/infoview/main.tsx @@ -27,7 +27,7 @@ import Markdown from '../markdown'; import { Infos } from './infos'; import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages'; 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 { InteractiveDiagnostic } from '@leanprover/infoview/*'; import { Button } from '../button'; @@ -349,7 +349,7 @@ export function TypewriterInterface({props}) { const [disableInput, setDisableInput] = React.useState(false) const [loadingProgress, setLoadingProgress] = React.useState(0) const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext) - const {mobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) const { proof } = React.useContext(ProofContext) const { setTypewriterInput } = React.useContext(InputModeContext) const { selectedStep, setSelectedStep } = React.useContext(SelectionContext) diff --git a/client/src/components/level.tsx b/client/src/components/level.tsx index 4b5a53d..dbd62a5 100644 --- a/client/src/components/level.tsx +++ b/client/src/components/level.tsx @@ -27,7 +27,7 @@ import { Button } from './button' import Markdown from './markdown' import {InventoryPanel} from './inventory' import { hasInteractiveErrors } from './infoview/typewriter' -import { DeletedChatContext, InputModeContext, MobileContext, MonacoEditorContext, +import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context' import { DualEditor } from './infoview/main' import { GameHint } from './infoview/rpc_api' @@ -74,7 +74,7 @@ function Level() { function ChatPanel({lastLevel}) { const chatRef = useRef(null) - const {mobile} = useContext(MobileContext) + const {mobile} = useContext(PreferencesContext) const gameId = useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId}) @@ -215,7 +215,7 @@ function PlayableLevel({impressum, setImpressum}) { const codeviewRef = useRef(null) const gameId = React.useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) - const {mobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) const dispatch = useAppDispatch() @@ -441,7 +441,7 @@ function PlayableLevel({impressum, setImpressum}) { function IntroductionPanel({gameInfo}) { const gameId = React.useContext(GameIdContext) const {worldId} = useContext(WorldLevelIdContext) - const {mobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) let text: Array = 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 */ function Introduction({impressum, setImpressum}) { const gameId = React.useContext(GameIdContext) - const {mobile} = useContext(MobileContext) + const {mobile} = useContext(PreferencesContext) const inventory = useLoadInventoryOverviewQuery({game: gameId}) diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx index 2b6d29b..aadc319 100644 --- a/client/src/components/popup/preferences.tsx +++ b/client/src/components/popup/preferences.tsx @@ -9,7 +9,7 @@ import FormControlLabel from '@mui/material/FormControlLabel'; import { IPreferencesContext } from "../infoview/context" -interface PreferencesPopupProps extends IPreferencesContext { +interface PreferencesPopupProps extends Omit { handleClose: () => void } diff --git a/client/src/components/welcome.tsx b/client/src/components/welcome.tsx index 18f9f9d..5d4b3de 100644 --- a/client/src/components/welcome.tsx +++ b/client/src/components/welcome.tsx @@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from '../hooks' import { changedOpenedIntro, selectOpenedIntro } from '../state/progress' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api' import { Button } from './button' -import { MobileContext, PreferencesContext } from './infoview/context' +import { PreferencesContext } from './infoview/context' import { InventoryPanel } from './inventory' import { ErasePopup } from './popup/erase' import { InfoPopup } from './popup/game_info' @@ -27,7 +27,7 @@ import { Hint } from './hints' /** the panel showing the game's introduction text */ function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) { - const {mobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) const gameId = React.useContext(GameIdContext) const dispatch = useAppDispatch() @@ -64,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, setMobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(PreferencesContext) const {layout, isSavePreferences, setLayout, setIsSavePreferences} = React.useContext(PreferencesContext) const gameInfo = useGetGameInfoQuery({game: gameId}) diff --git a/client/src/components/world_tree.tsx b/client/src/components/world_tree.tsx index 76cf5fb..65fcc8a 100644 --- a/client/src/components/world_tree.tsx +++ b/client/src/components/world_tree.tsx @@ -16,7 +16,7 @@ import { selectDifficulty, changedDifficulty, selectCompleted } from '../state/p import { store } from '../state/store' import '../css/world_tree.css' -import { MobileContext } from './infoview/context' +import { PreferencesContext } from './infoview/context' // Settings for the world tree cytoscape.use( klay ) @@ -198,7 +198,7 @@ export function WorldSelectionMenu({rulesHelp, setRulesHelp}) { const gameId = React.useContext(GameIdContext) const difficulty = useSelector(selectDifficulty(gameId)) const dispatch = useAppDispatch() - const { mobile } = React.useContext(MobileContext) + const { mobile } = React.useContext(PreferencesContext) function label(x : number) { diff --git a/client/src/state/hooks/use_preferences.ts b/client/src/state/hooks/use_preferences.ts new file mode 100644 index 0000000..3b4f66c --- /dev/null +++ b/client/src/state/hooks/use_preferences.ts @@ -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() + + 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; \ No newline at end of file diff --git a/client/src/state/preferences.ts b/client/src/state/preferences.ts index f3c3fbb..7ccbe30 100644 --- a/client/src/state/preferences.ts +++ b/client/src/state/preferences.ts @@ -26,10 +26,10 @@ export const preferencesSlice = createSlice({ setLayout: (state, action) => { state.layout = action.payload; }, - setisSavePreferences: (state, action) => { + setIsSavePreferences: (state, action) => { state.isSavePreferences = action.payload; }, }, }); -export const { setLayout, setisSavePreferences } = preferencesSlice.actions; +export const { setLayout, setIsSavePreferences } = preferencesSlice.actions;