From 7515561883f8f70f390f4c34ee09e9f31b2fbe70 Mon Sep 17 00:00:00 2001 From: ran Date: Tue, 12 Dec 2023 19:17:55 +0800 Subject: [PATCH 1/3] Add a button to toggle mobile --- client/src/components/app_bar.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/client/src/components/app_bar.tsx b/client/src/components/app_bar.tsx index 2c65a81..d07f80f 100644 --- a/client/src/components/app_bar.tsx +++ b/client/src/components/app_bar.tsx @@ -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 } from '@fortawesome/free-solid-svg-icons' import { GameIdContext } from "../app" import { InputModeContext, MobileContext, WorldLevelIdContext } from "./infoview/context" import { GameInfo, useGetGameInfoQuery } from '../state/api' @@ -161,7 +161,7 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres }) { 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
@@ -182,6 +182,12 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres + {mobile ? : + } @@ -208,7 +214,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, setMobile} = React.useContext(MobileContext) const [navOpen, setNavOpen] = React.useState(false) const gameInfo = useGetGameInfoQuery({game: gameId}) const completed = useAppSelector(selectCompleted(gameId, worldId, levelId)) @@ -231,6 +237,9 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= +
@@ -248,6 +257,9 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= + From 47d7f606c6f350a70cd41024d0125169194df6e4 Mon Sep 17 00:00:00 2001 From: ran Date: Wed, 13 Dec 2023 00:34:38 +0800 Subject: [PATCH 2/3] Add basic preference framework and mobile options --- client/src/app.tsx | 26 +++++++++++++++++++---- client/src/components/app_bar.tsx | 24 +++++++-------------- client/src/components/infoview/context.ts | 12 ++++++++--- client/src/components/welcome.tsx | 11 ++++++++-- client/src/hooks.ts | 24 +++++++++++++++++++++ client/src/state/local_storage.ts | 21 ++++++++++++++++++ client/src/state/store.ts | 5 ++++- client/src/window_width.tsx | 21 ------------------ 8 files changed, 97 insertions(+), 47 deletions(-) delete mode 100644 client/src/window_width.tsx diff --git a/client/src/app.tsx b/client/src/app.tsx index c45f6d0..653d0db 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -9,20 +9,38 @@ 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'; + export const GameIdContext = React.createContext(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]) return (
- + diff --git a/client/src/components/app_bar.tsx b/client/src/components/app_bar.tsx index d07f80f..c766b52 100644 --- a/client/src/components/app_bar.tsx +++ b/client/src/components/app_bar.tsx @@ -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, faMobileScreenButton, faDesktop } 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,14 +150,15 @@ 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)) @@ -182,12 +183,6 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres - {mobile ? : - } @@ -200,6 +195,9 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres +
} @@ -214,7 +212,7 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= }) { const gameId = React.useContext(GameIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext) - const {mobile, setMobile} = React.useContext(MobileContext) + const {mobile} = React.useContext(MobileContext) const [navOpen, setNavOpen] = React.useState(false) const gameInfo = useGetGameInfoQuery({game: gameId}) const completed = useAppSelector(selectCompleted(gameId, worldId, levelId)) @@ -237,9 +235,6 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= - @@ -257,9 +252,6 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= - diff --git a/client/src/components/infoview/context.ts b/client/src/components/infoview/context.ts index fb0ef2b..8a62b36 100644 --- a/client/src/components/infoview/context.ts +++ b/client/src/components/infoview/context.ts @@ -62,12 +62,18 @@ export const ProofStateContext = React.createContext<{ setProofState: () => {}, }) -export const MobileContext = React.createContext<{ +export interface IMobileContext { mobile : boolean, setMobile: React.Dispatch>, -}>({ - mobile : false, + 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/welcome.tsx b/client/src/components/welcome.tsx index 6c6c977..89b9adc 100644 --- a/client/src/components/welcome.tsx +++ b/client/src/components/welcome.tsx @@ -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() { : <> + toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>
{ mobile ?
@@ -128,6 +134,7 @@ function Welcome() { {eraseMenu? : null} {uploadMenu? : null} {info ? : null} + {preferencesPopup ? : null} } diff --git a/client/src/hooks.ts b/client/src/hooks.ts index afdce0a..0ac6e2f 100644 --- a/client/src/hooks.ts +++ b/client/src/hooks.ts @@ -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 = 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 6e88db2..ea165ce 100644 --- a/client/src/state/local_storage.ts +++ b/client/src/state/local_storage.ts @@ -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 + } +} diff --git a/client/src/state/store.ts b/client/src/state/store.ts index 4079ebd..a2294de 100644 --- a/client/src/state/store.ts +++ b/client/src/state/store.ts @@ -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) ); diff --git a/client/src/window_width.tsx b/client/src/window_width.tsx deleted file mode 100644 index 9e8b484..0000000 --- a/client/src/window_width.tsx +++ /dev/null @@ -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 -} From 9f692ccf619d2cc3172fb0b1a16d3c6223b602ea Mon Sep 17 00:00:00 2001 From: ran Date: Wed, 13 Dec 2023 00:37:50 +0800 Subject: [PATCH 3/3] Add basic preference framework and mobile options and Add missing files --- client/src/components/popup/preferences.tsx | 55 +++++++++++++++++++++ client/src/state/preferences.ts | 37 ++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 client/src/components/popup/preferences.tsx create mode 100644 client/src/state/preferences.ts diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx new file mode 100644 index 0000000..73b2dbe --- /dev/null +++ b/client/src/components/popup/preferences.tsx @@ -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
+
+
+
+ +
+
+

Mobile layout

+
+
+ setMobile(!mobile)} + name="checked" + color="primary" + /> + } + label="Enable" + labelPlacement="start" + /> +
+
+ setLockMobile(!lockMobile)} + name="checked" + color="primary" + /> + } + label="Auto" + labelPlacement="start" + /> +
+
+
+
+
+} diff --git a/client/src/state/preferences.ts b/client/src/state/preferences.ts new file mode 100644 index 0000000..6ee0fa1 --- /dev/null +++ b/client/src/state/preferences.ts @@ -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;