@@ -15,6 +15,8 @@ import ListItem from '@mui/material/ListItem';
1515import ListItemText from '@mui/material/ListItemText' ;
1616import ListItemAvatar from '@mui/material/ListItemAvatar' ;
1717import Divider from '@mui/material/Divider' ;
18+ import CssBaseline from '@mui/material/CssBaseline' ;
19+ import IconButton from '@mui/material/IconButton' ;
1820import React from 'react' ;
1921import General from './page/General.jsx' ;
2022import Engines from './page/Engines.jsx' ;
@@ -24,6 +26,9 @@ import FindInPage from './page/FindInPage.jsx';
2426import Link from '@mui/material/Link' ;
2527import Snackbar from '@mui/material/Snackbar' ;
2628import MuiAlert from '@mui/material/Alert' ;
29+ import { ThemeProvider , createTheme } from '@mui/material/styles' ;
30+ import DarkModeIcon from '@mui/icons-material/DarkMode' ;
31+ import WbSunnyIcon from '@mui/icons-material/WbSunny' ;
2732import { createClient } from "webdav" ;
2833import { version } from './Version.js' ;
2934
@@ -141,11 +146,36 @@ function a11yProps(index: number) {
141146export default function App ( ) {
142147 const [ value , setValue ] = React . useState ( 4 ) ;
143148 const [ inited , setInited ] = React . useState ( false ) ;
149+ const [ darkMode , setDarkMode ] = React . useState ( ( ) => {
150+ try {
151+ return localStorage . getItem ( 'sj-dark-mode' ) === '1' ;
152+ } catch ( e ) {
153+ return false ;
154+ }
155+ } ) ;
144156
145157 const handleChange = ( event : React . SyntheticEvent , newValue : number ) => {
146158 setValue ( newValue ) ;
147159 document . documentElement . scrollTop = 0 ;
148160 } ;
161+ const handleDarkModeToggle = ( ) => {
162+ setDarkMode ( prev => {
163+ const nextMode = ! prev ;
164+ try {
165+ localStorage . setItem ( 'sj-dark-mode' , nextMode ? '1' : '0' ) ;
166+ } catch ( e ) {
167+ // Ignore storage failures (private mode, disabled storage).
168+ }
169+ return nextMode ;
170+ } ) ;
171+ } ;
172+ const theme = React . useMemo ( ( ) => {
173+ return createTheme ( {
174+ palette : {
175+ mode : darkMode ? 'dark' : 'light'
176+ }
177+ } ) ;
178+ } , [ darkMode ] ) ;
149179 const [ alertBody , setAlert ] = React . useState ( { openAlert : false , alertContent : '' , alertType : 'error' } ) ;
150180 const handleAlertOpen = ( content , type ) => {
151181 switch ( type ) {
@@ -210,87 +240,104 @@ export default function App() {
210240 return true ;
211241 } , true ) ;
212242 } , [ ] )
243+ React . useEffect ( ( ) => {
244+ document . body . dataset . theme = darkMode ? 'dark' : 'light' ;
245+ } , [ darkMode ] ) ;
213246
214247 return (
215- < Box
216- inited = { inited }
217- sx = { { flexGrow : 1 , bgcolor : 'background.paper' , display : 'flex' , height : '100vh' , marginLeft : '200px' } }
218- >
219- < List
220- id = "tabs"
221- component = { Paper }
222- elevation = { 5 }
223- sx = { {
224- width : '200px' ,
225- height : '100%' ,
226- left : 0 ,
227- maxWidth : 200 ,
228- position : 'fixed'
229- } }
248+ < ThemeProvider theme = { theme } >
249+ < CssBaseline />
250+ < Box
251+ inited = { inited }
252+ sx = { { flexGrow : 1 , bgcolor : 'background.paper' , display : 'flex' , height : '100vh' , marginLeft : '200px' } }
230253 >
231- < ListItem
232- onClick = { ( ) => {
233- document . querySelector ( '#tabs' ) . classList . remove ( 'hide' ) ;
254+ < List
255+ id = "tabs"
256+ component = { Paper }
257+ elevation = { 5 }
258+ sx = { {
259+ width : '200px' ,
260+ height : '100%' ,
261+ left : 0 ,
262+ maxWidth : 200 ,
263+ position : 'fixed'
234264 } }
235265 >
236- < ListItemAvatar >
237- < Link href = 'https://github.com/hoothin/SearchJumper' target = "_blank" >
238- < Avatar alt = "SearchJumper" component = { Paper } elevation = { 5 } src = { logo } />
239- </ Link >
240- </ ListItemAvatar >
241- < ListItemText
242- primary = { window . i18n ( 'name' ) }
243- secondary = { window . version ? ( "Ver " + window . version ) : ( window . version === 0 ? "" : "Not installed" ) }
244- sx = { { cursor : 'pointer' } }
245- onClick = { e => { inited && window . version !== 0 && window . version !== version && window . open ( "https://greasyfork.org/scripts/445274-searchjumper/code/SearchJumper.user.js" ) } }
246- secondaryTypographyProps = { inited && window . version !== version ? {
247- sx :{ color : 'red' } ,
248- title :window . i18n ( 'outOfDate' )
249- } : ( ! inited ? {
250- sx :{ color : 'red' }
251- } : { } ) } />
252- </ ListItem >
253- < Divider component = "li" variant = "inset" sx = { { marginRight : 3 } } />
254- < ListItem sx = { { flexFlow : 'column' , height : 'calc(100% - 75px)' , overflowY : 'auto' , overflowX : 'hidden' } } >
255- < Tabs
256- orientation = "vertical"
257- variant = "scrollable"
258- scrollButtons = "auto"
259- value = { value }
260- onChange = { handleChange }
261- onClick = { ( ) => { document . querySelector ( '#tabs' ) . classList . add ( 'hide' ) } }
262- aria-label = "Vertical tabs example"
263- sx = { { borderRight : 1 , borderColor : 'divider' , width : '100%' , flexShrink : 0 } }
266+ < ListItem
267+ onClick = { ( ) => {
268+ document . querySelector ( '#tabs' ) . classList . remove ( 'hide' ) ;
269+ } }
264270 >
265- < Tab label = { window . i18n ( 'general' ) } { ...a11yProps ( 0 ) } />
266- < Tab label = { window . i18n ( 'searchEngines' ) } { ...a11yProps ( 1 ) } />
267- < Tab label = { window . i18n ( 'findInPage' ) } { ...a11yProps ( 2 ) } />
268- < Tab label = { window . i18n ( 'exportConfig' ) } { ...a11yProps ( 3 ) } />
269- < Tab label = { window . i18n ( 'about' ) } { ...a11yProps ( 4 ) } />
270- </ Tabs >
271- { / ^ ( h t t p | f t p ) / i. test ( window . location . protocol ) ? < embed className = "sponsors" wmode = "transparent" src = "https://search.hoothin.com/sjsponsors.svg" /> : < div id = "sponsors" className = "sponsors" > </ div > }
272- </ ListItem >
273- </ List >
274- < TabPanel value = { value } index = { 0 } sx = { { width :1 } } >
275- { window . searchData ? < General /> : < About /> }
276- </ TabPanel >
277- < TabPanel value = { value } index = { 1 } >
278- { window . searchData ? < Engines /> : < About /> }
279- </ TabPanel >
280- < TabPanel value = { value } index = { 2 } >
281- { window . searchData ? < FindInPage /> : < About /> }
282- </ TabPanel >
283- < TabPanel value = { value } index = { 3 } >
284- { window . searchData ? < Export /> : < About /> }
285- </ TabPanel >
286- < TabPanel value = { value } index = { 4 } >
287- < About />
288- </ TabPanel >
289- < Snackbar open = { alertBody . openAlert } autoHideDuration = { 5000 } anchorOrigin = { { vertical : 'top' , horizontal : 'center' } } onClose = { handleAlertClose } >
290- < MuiAlert elevation = { 6 } variant = "filled" onClose = { handleAlertClose } severity = { alertBody . alertType } sx = { { width : '100%' } } >
291- { alertBody . alertContent }
292- </ MuiAlert >
293- </ Snackbar >
294- </ Box >
271+ < ListItemAvatar >
272+ < Link href = 'https://github.com/hoothin/SearchJumper' target = "_blank" >
273+ < Avatar alt = "SearchJumper" component = { Paper } elevation = { 5 } src = { logo } />
274+ </ Link >
275+ </ ListItemAvatar >
276+ < ListItemText
277+ primary = { window . i18n ( 'name' ) }
278+ secondary = { window . version ? ( "Ver " + window . version ) : ( window . version === 0 ? "" : "Not installed" ) }
279+ sx = { { cursor : 'pointer' } }
280+ onClick = { e => { inited && window . version !== 0 && window . version !== version && window . open ( "https://greasyfork.org/scripts/445274-searchjumper/code/SearchJumper.user.js" ) } }
281+ secondaryTypographyProps = { inited && window . version !== version ? {
282+ sx :{ color : 'red' } ,
283+ title :window . i18n ( 'outOfDate' )
284+ } : ( ! inited ? {
285+ sx :{ color : 'red' }
286+ } : { } ) } />
287+ </ ListItem >
288+ < Divider component = "li" variant = "inset" sx = { { marginRight : 3 } } />
289+ < ListItem sx = { { flexFlow : 'column' , height : 'calc(100% - 75px)' , overflowY : 'auto' , overflowX : 'hidden' } } >
290+ < Tabs
291+ orientation = "vertical"
292+ variant = "scrollable"
293+ scrollButtons = "auto"
294+ value = { value }
295+ onChange = { handleChange }
296+ onClick = { ( ) => { document . querySelector ( '#tabs' ) . classList . add ( 'hide' ) } }
297+ aria-label = "Vertical tabs example"
298+ sx = { { borderRight : 1 , borderColor : 'divider' , width : '100%' , flexShrink : 0 } }
299+ >
300+ < Tab label = { window . i18n ( 'general' ) } { ...a11yProps ( 0 ) } />
301+ < Tab label = { window . i18n ( 'searchEngines' ) } { ...a11yProps ( 1 ) } />
302+ < Tab label = { window . i18n ( 'findInPage' ) } { ...a11yProps ( 2 ) } />
303+ < Tab label = { window . i18n ( 'exportConfig' ) } { ...a11yProps ( 3 ) } />
304+ < Tab label = { window . i18n ( 'about' ) } { ...a11yProps ( 4 ) } />
305+ </ Tabs >
306+ < Box sx = { { width : '100%' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , padding : '4px 8px' } } >
307+ < IconButton
308+ size = "small"
309+ onClick = { handleDarkModeToggle }
310+ disabled = { ! inited }
311+ aria-label = "Dark mode"
312+ title = "Dark mode"
313+ >
314+ { darkMode ? < WbSunnyIcon fontSize = "small" /> : < DarkModeIcon fontSize = "small" /> }
315+ </ IconButton >
316+ </ Box >
317+ { / ^ ( h t t p | f t p ) / i. test ( window . location . protocol ) ? < embed className = "sponsors" wmode = "transparent" src = "https://search.hoothin.com/sjsponsors.svg" /> : < div id = "sponsors" className = "sponsors" > </ div > }
318+ </ ListItem >
319+ </ List >
320+ < TabPanel value = { value } index = { 0 } sx = { { width :1 } } >
321+ { window . searchData ? < General /> : < About /> }
322+ </ TabPanel >
323+ < TabPanel value = { value } index = { 1 } >
324+ { window . searchData ? < Engines /> : < About /> }
325+ </ TabPanel >
326+ < TabPanel value = { value } index = { 2 } >
327+ { window . searchData ? < FindInPage /> : < About /> }
328+ </ TabPanel >
329+ < TabPanel value = { value } index = { 3 } >
330+ { window . searchData ? < Export /> : < About /> }
331+ </ TabPanel >
332+ < TabPanel value = { value } index = { 4 } >
333+ < About />
334+ </ TabPanel >
335+ < Snackbar open = { alertBody . openAlert } autoHideDuration = { 5000 } anchorOrigin = { { vertical : 'top' , horizontal : 'center' } } onClose = { handleAlertClose } >
336+ < MuiAlert elevation = { 6 } variant = "filled" onClose = { handleAlertClose } severity = { alertBody . alertType } sx = { { width : '100%' } } >
337+ { alertBody . alertContent }
338+ </ MuiAlert >
339+ </ Snackbar >
340+ </ Box >
341+ </ ThemeProvider >
295342 ) ;
296343}
0 commit comments