diff --git a/.gitignore b/.gitignore
index 9e7be89b..edd71bf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,4 @@ dist/
build/
.nyc_output/
coverage/
-!.vscode/extensions.json
\ No newline at end of file
+!.vscode/extensions.json
diff --git a/TODO.md b/TODO.md
index 023979c1..8b78e33b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -11,6 +11,7 @@
- [x] Get clear on paths
- [x] ESLint root package dependency usage
- [x] Move themes to `packages/themes`
+- [ ] Separated server vs client settings + interface (new events?)
## Backend
@@ -35,8 +36,11 @@
- [x] Move off CRA to Vite
- [ ] Separate out line jump into per file
- [x] Convert to TS
-- [ ] Refactor routing to be centralised (per feature)
- [ ] Sort out `react-hotkeys` usage (error regarding no `parentId`)
+- [ ] Use new events from BE
+- [ ] Refactor into concept of `content`
+- [ ] Render navigator's bar extra icons in controller
+- [ ] Better settings management via hooks
## Electron
diff --git a/apps/frontend/.eslintrc.cjs b/apps/frontend/.eslintrc.cjs
index 0502dd87..4b88f2df 100644
--- a/apps/frontend/.eslintrc.cjs
+++ b/apps/frontend/.eslintrc.cjs
@@ -4,4 +4,12 @@ module.exports = {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
+ overrides: [ {
+ // Targeting route files
+ files: [ '**/*/app/**/*.ts?(x)' ],
+ rules: {
+ // Referencing the exported route leads to a use before define in components, but it's fine
+ '@typescript-eslint/no-use-before-define': 'off',
+ },
+ } ],
}
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index f4db177e..3e857c7a 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -22,6 +22,8 @@
"@mui/material": "^5.16.6",
"@presenter/themes": "*",
"@sentry/browser": "^6.19.7",
+ "@tanstack/react-router": "^1.49.2",
+ "@tanstack/router-zod-adapter": "^1.51.6",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.1",
"detect-browser": "^5.3.0",
@@ -31,21 +33,21 @@
"lodash": "^4.17.21",
"memoizee": "^0.4.17",
"notistack": "^3.0.1",
- "qs": "^6.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hotkeys": "^2.0.0",
"react-idle-timer": "^5.7.2",
- "react-router-dom": "^6.26.0",
"react-transition-group": "^4.4.5",
"reconnecting-websocket": "^4.4.0",
- "scroll-into-view": "^1.16.0"
+ "scroll-into-view": "^1.16.0",
+ "zod": "^3.8.2-alpha.6"
},
"devDependencies": {
"@presenter/contract": "*",
+ "@tanstack/router-devtools": "^1.49.2",
+ "@tanstack/router-plugin": "^1.49.3",
"@types/lodash": "^4.17.7",
"@types/memoizee": "^0.4.11",
- "@types/qs": "^6.9.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/scroll-into-view": "^1.16.0",
diff --git a/apps/frontend/src/App.css b/apps/frontend/src/app/__root.css
similarity index 82%
rename from apps/frontend/src/App.css
rename to apps/frontend/src/app/__root.css
index 1da7e4a5..f139a2d9 100644
--- a/apps/frontend/src/App.css
+++ b/apps/frontend/src/app/__root.css
@@ -1,6 +1,6 @@
-@import url('./fonts/noto-sans/index.css');
-@import url('./fonts/OpenGurbaniAkhar/index.css');
-@import url('./fonts/OpenAnmolUni/index.css');
+@import url('../fonts/noto-sans/index.css');
+@import url('../fonts/OpenGurbaniAkhar/index.css');
+@import url('../fonts/OpenAnmolUni/index.css');
@import url('@fontsource/roboto');
/* Shared CSS constants */
diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/app/__root.tsx
similarity index 73%
rename from apps/frontend/src/App.tsx
rename to apps/frontend/src/app/__root.tsx
index 6165e273..c796ed73 100644
--- a/apps/frontend/src/App.tsx
+++ b/apps/frontend/src/app/__root.tsx
@@ -1,14 +1,13 @@
-import './App.css'
+import './__root.css'
import { RecommendedSources, Shabad, Writer } from '@presenter/contract'
+import { createRootRoute, Navigate, Outlet } from '@tanstack/react-router'
import classNames from 'classnames'
import { SnackbarProvider } from 'notistack'
-import { ElementType, lazy, PureComponent, ReactNode, Suspense } from 'react'
-import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
+import { PureComponent, Suspense } from 'react'
import Loader from '~/components/Loader'
-import Overlay from '~/features/overlay'
-import { BACKEND_URL, isDesktop, isMobile, isTablet, OVERLAY_URL, PRESENTER_URL, SCREEN_READER_URL, SETTINGS_URL } from '~/helpers/consts'
+import { API_URL, isDesktop, isMobile, isTablet } from '~/helpers/consts'
import {
BookmarksContext,
ContentContext,
@@ -22,20 +21,9 @@ import { DEFAULT_OPTIONS } from '~/helpers/options'
import { merge } from '~/helpers/utils'
import controller from '~/services/controller'
-const ScreenReader = lazy( () => import( '~/features/screen-reader' ) )
-const Presenter = lazy( () => import( '~/features/presenter' ) )
-const Settings = lazy( () => import( '~/features/settings' ) )
-
const loadSettings = () => merge( { local: controller.readSettings() }, DEFAULT_OPTIONS )
class App extends PureComponent {
- components = [
- [ Overlay, OVERLAY_URL ],
- [ ScreenReader, SCREEN_READER_URL ],
- [ Settings, SETTINGS_URL ],
- [ Presenter, PRESENTER_URL ],
- ] as const
-
state = {
connected: false,
connectedAt: null,
@@ -72,7 +60,7 @@ class App extends PureComponent {
controller.on( 'settings', this.onSettings )
// Get recommended sources and set as settings, if there are none
- void fetch( `${BACKEND_URL}/sources` )
+ void fetch( `${API_URL}/sources` )
.then( ( res ) => res.json() )
.then( ( { recommendedSources }: { recommendedSources: RecommendedSources['recommendedSources'] } ) => {
//* Update default options and settings with fetched recommended sources
@@ -82,7 +70,7 @@ class App extends PureComponent {
} )
// Get writers
- void fetch( `${BACKEND_URL}/writers` )
+ void fetch( `${API_URL}/writers` )
.then( ( res ) => res.json() )
.then( ( { writers }: { writers: Writer[] } ) => this.setState( { writers } ) )
}
@@ -172,37 +160,33 @@ class App extends PureComponent {
settings,
} = this.state
- // Generate a context wrapper function
- const withContexts = ( [
- [ StatusContext.Provider, { connected, connectedAt, status } ],
- [ SettingsContext.Provider, settings ],
- [ HistoryContext.Provider, { viewedLines, transitionHistory, latestLines } ],
- [ BookmarksContext.Provider, banis ],
- [ WritersContext.Provider, writers ],
- [ RecommendedSourcesContext.Provider, recommendedSources ],
- [ ContentContext.Provider, { bani, shabad, lineId, mainLineId, nextLineId } ],
- [ SnackbarProvider ],
- ] as [ElementType, any][] )
- .reduce( ( withContexts, [ Provider, value ] ) => ( children ) => withContexts(
-
- {children}
- ,
- ), ( context: ReactNode ) => context )
-
- return withContexts(
+ return (
}>
-
-
- {this.components.map( ( [ Component, path ] ) => (
- } />
- ) )}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
,
+
)
}
}
-export default App
+export const Route = createRootRoute( {
+ component: App,
+ notFoundComponent: () => ,
+} )
diff --git a/apps/frontend/src/app/index.tsx b/apps/frontend/src/app/index.tsx
new file mode 100644
index 00000000..91fef4da
--- /dev/null
+++ b/apps/frontend/src/app/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute( '/' )( {
+ beforeLoad: () => redirect( { to: '/presenter', throw: true } ),
+} )
diff --git a/apps/frontend/src/features/overlay/Line.css b/apps/frontend/src/app/overlay/-components/Line/index.css
similarity index 100%
rename from apps/frontend/src/features/overlay/Line.css
rename to apps/frontend/src/app/overlay/-components/Line/index.css
diff --git a/apps/frontend/src/features/overlay/Line.tsx b/apps/frontend/src/app/overlay/-components/Line/index.tsx
similarity index 94%
rename from apps/frontend/src/features/overlay/Line.tsx
rename to apps/frontend/src/app/overlay/-components/Line/index.tsx
index c7aa249b..32da4f88 100644
--- a/apps/frontend/src/features/overlay/Line.tsx
+++ b/apps/frontend/src/app/overlay/-components/Line/index.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react/no-array-index-key */
-import './Line.css'
+import './index.css'
import classNames from 'classnames'
@@ -14,12 +14,9 @@ type LineProps = {
larivaarGurbani?: boolean,
larivaarAssist?: boolean,
}
-/**
- * Overlay Line Component.
- * Renders the various aspects of a single line.
- */
+
const Line = ( {
- className = '',
+ className,
gurmukhi,
larivaarGurbani: larivaar = false,
larivaarAssist = false,
diff --git a/apps/frontend/src/features/overlay/ThemeLoader.tsx b/apps/frontend/src/app/overlay/-components/ThemeLoader.tsx
similarity index 68%
rename from apps/frontend/src/features/overlay/ThemeLoader.tsx
rename to apps/frontend/src/app/overlay/-components/ThemeLoader.tsx
index 0e68a5a0..421256e4 100644
--- a/apps/frontend/src/features/overlay/ThemeLoader.tsx
+++ b/apps/frontend/src/app/overlay/-components/ThemeLoader.tsx
@@ -1,15 +1,12 @@
import { useContext } from 'react'
-import { OVERLAY_THEMES_URL } from '~/helpers/consts'
+import { API_URL } from '~/helpers/consts'
import { StatusContext } from '~/helpers/contexts'
+const OVERLAY_THEMES_URL = `${API_URL}/themes/overlay`
+
type ThemeLoaderProps = { name: string }
-/**
- * Component to load a theme using a `` tag.
- * @param name The name of the CSS theme to load from the server.
- * @constructor
- */
const ThemeLoader = ( { name }: ThemeLoaderProps ) => {
const { connectedAt } = useContext( StatusContext )
diff --git a/apps/frontend/src/features/overlay/index.css b/apps/frontend/src/app/overlay/index.css
similarity index 100%
rename from apps/frontend/src/features/overlay/index.css
rename to apps/frontend/src/app/overlay/index.css
diff --git a/apps/frontend/src/features/overlay/index.tsx b/apps/frontend/src/app/overlay/index.lazy.tsx
similarity index 88%
rename from apps/frontend/src/features/overlay/index.tsx
rename to apps/frontend/src/app/overlay/index.lazy.tsx
index 1592ad53..2f0c121c 100644
--- a/apps/frontend/src/features/overlay/index.tsx
+++ b/apps/frontend/src/app/overlay/index.lazy.tsx
@@ -1,5 +1,4 @@
-import './index.css'
-
+import { createLazyFileRoute } from '@tanstack/react-router'
import classNames from 'classnames'
import { mapValues } from 'lodash'
import { useContext } from 'react'
@@ -10,8 +9,8 @@ import { customiseLine, getTransliterators } from '~/helpers/line'
import { filterFalsyValues } from '~/helpers/utils'
import { useCurrentLine, useTranslations } from '~/hooks'
-import Line from './Line'
-import ThemeLoader from './ThemeLoader'
+import Line from './-components/Line'
+import ThemeLoader from './-components/ThemeLoader'
const Overlay = () => {
const settings = useContext( SettingsContext )
@@ -60,4 +59,6 @@ const Overlay = () => {
)
}
-export default Overlay
+export const Route = createLazyFileRoute( '/overlay/' )( {
+ component: Overlay,
+} )
diff --git a/apps/frontend/src/features/presenter/Display.css b/apps/frontend/src/app/presenter/-components/Display/index.css
similarity index 100%
rename from apps/frontend/src/features/presenter/Display.css
rename to apps/frontend/src/app/presenter/-components/Display/index.css
diff --git a/apps/frontend/src/features/presenter/Display.tsx b/apps/frontend/src/app/presenter/-components/Display/index.tsx
similarity index 93%
rename from apps/frontend/src/features/presenter/Display.tsx
rename to apps/frontend/src/app/presenter/-components/Display/index.tsx
index 6aab02cd..e0d84658 100644
--- a/apps/frontend/src/features/presenter/Display.tsx
+++ b/apps/frontend/src/app/presenter/-components/Display/index.tsx
@@ -1,4 +1,4 @@
-import './Display.css'
+import './index.css'
import classNames from 'classnames'
import { mapValues } from 'lodash'
@@ -9,18 +9,12 @@ import { ClientSettings } from '~/helpers/options'
import { filterFalsyValues } from '~/helpers/utils'
import { useCurrentLine, useCurrentLines, useTranslations } from '~/hooks'
-import Line from './Line'
+import Line from '../Line'
type DisplayProps = {
settings: Pick,
}
-/**
- * Display Component.
- * Displays the current Shabad, with visual settings.
- * @param shabad The Shabad to render.
- * @param lineId The current line in the Shabad.
- */
const Display = ( { settings }: DisplayProps ) => {
const {
layout,
diff --git a/apps/frontend/src/features/presenter/Line.css b/apps/frontend/src/app/presenter/-components/Line/index.css
similarity index 100%
rename from apps/frontend/src/features/presenter/Line.css
rename to apps/frontend/src/app/presenter/-components/Line/index.css
diff --git a/apps/frontend/src/features/presenter/Line.tsx b/apps/frontend/src/app/presenter/-components/Line/index.tsx
similarity index 82%
rename from apps/frontend/src/features/presenter/Line.tsx
rename to apps/frontend/src/app/presenter/-components/Line/index.tsx
index a417f10a..7ef2c430 100644
--- a/apps/frontend/src/features/presenter/Line.tsx
+++ b/apps/frontend/src/app/presenter/-components/Line/index.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react/no-array-index-key */
-import './Line.css'
+import './index.css'
import classNames from 'classnames'
import { countSyllables, toSyllabicSymbols } from 'gurmukhi-utils'
@@ -77,32 +77,6 @@ const {
theme: { simpleGraphics },
} = DEFAULT_OPTIONS.local
-/**
- * Line Component.
- * Renders the various aspects of a single line.
- * @param {string} className An optional class name to append.
- * @param {string} gurmukhi The Gurmukhi of the line to render.
- * @param {string} translations The Punjabi translation of the line to render.
- * @param {string} transliterators The Punjabi translation of the line to render.
- * @param {string} spacing The justify content value for spacing between the lines.
- * @param {boolean} centerText Whether to center text.
- * @param {boolean} justifyText Whether to justify (edge to edge) wrapped text (2+ lines long).
- * @param {number} presenterFontSize The global font size of presenter lines.
- * @param {number} relativeGurmukhiFontSize Relative size for gurmukhi ascii font.
- * @param {number} relativeEnglishFontSize Relative size for latin scripts (english/spanish).
- * @param {number} relativePunjabiFontSize Relative size for punjabi unicode font.
- * @param {number} relativeHindiFontSize Relative font size for hindi unicode font.
- * @param {number} relativeUrduFontSize Relative font size for urdu unicode font.
- * @param {boolean} larivaarGurbani Whether Gurbani should be continuous or not.
- * @param {boolean} larivaarAssist If `larivaarGurbani`, whether alternate words should be coloured.
- * @param {boolean} vishraamColors Enables colors for vishraams.
- * @param {boolean} vishraamCharacters Enables display of vishraam characters.
- * @param {boolean} vishraamLight Enables colors for light vishraams.
- * @param {boolean} vishraamMedium Enables colors for medium vishraams.
- * @param {boolean} vishraamHeavy Enables colors for heavy vishraams.
- * @param {boolean} splitOnVishraam If the line is too long, split it on the vishraam word.
- * @param {boolean} simpleGraphics Disables transitions and other intensive effects.
- */
const Line = ( {
className = undefined,
gurmukhi,
diff --git a/apps/frontend/src/features/presenter/StatusToast.css b/apps/frontend/src/app/presenter/-components/StatusToast/index.css
similarity index 100%
rename from apps/frontend/src/features/presenter/StatusToast.css
rename to apps/frontend/src/app/presenter/-components/StatusToast/index.css
diff --git a/apps/frontend/src/features/presenter/StatusToast.tsx b/apps/frontend/src/app/presenter/-components/StatusToast/index.tsx
similarity index 96%
rename from apps/frontend/src/features/presenter/StatusToast.tsx
rename to apps/frontend/src/app/presenter/-components/StatusToast/index.tsx
index 5b8454bd..bf47f73f 100644
--- a/apps/frontend/src/features/presenter/StatusToast.tsx
+++ b/apps/frontend/src/app/presenter/-components/StatusToast/index.tsx
@@ -1,4 +1,4 @@
-import './StatusToast.css'
+import './index.css'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
diff --git a/apps/frontend/src/app/presenter/-controller-location-history.ts b/apps/frontend/src/app/presenter/-controller-location-history.ts
new file mode 100644
index 00000000..a6af6175
--- /dev/null
+++ b/apps/frontend/src/app/presenter/-controller-location-history.ts
@@ -0,0 +1,9 @@
+import createUseLocationHistory from '~/hooks/use-location-history'
+
+export const {
+ Provider: ControllerLocationHistoryProvider,
+ useLocationHistory: useControllerLocationHistory,
+} = createUseLocationHistory(
+ '/presenter/controller/search',
+ { filter: ( href ) => !!href.match( '/presenter/controller/.*' ) }
+)
diff --git a/apps/frontend/src/features/controller/ToolbarButton.tsx b/apps/frontend/src/app/presenter/controller/-components/ToolbarButton.tsx
similarity index 60%
rename from apps/frontend/src/features/controller/ToolbarButton.tsx
rename to apps/frontend/src/app/presenter/controller/-components/ToolbarButton.tsx
index 8151e034..bc2fbd71 100644
--- a/apps/frontend/src/features/controller/ToolbarButton.tsx
+++ b/apps/frontend/src/app/presenter/controller/-components/ToolbarButton.tsx
@@ -9,17 +9,9 @@ type ToolbarButtonProps = {
onClick?: () => void,
onMouseEnter?: () => void,
onMouseLeave?: () => void,
+ flip?: 'horizontal' | 'vertical',
}
-/**
- * Renders an individual icon button, setting the state with the name on hover and click.
- * @param {string} name The human-readable name of the icon.
- * @param {string} icon The font-awesome icon.
- * @param {Function} onClick Optional click handler.
- * @param {Function} onMouseEnter MouseEnter click handler.
- * @param {Function} onMouseLeave MouseLeave click handler.
- * @param {string} className Optional classname.
- */
const ToolbarButton = ( {
name,
icon,
@@ -27,6 +19,7 @@ const ToolbarButton = ( {
onMouseEnter,
onMouseLeave,
className,
+ flip,
}: ToolbarButtonProps ) => (
-
+
)
diff --git a/apps/frontend/src/features/controller/Bookmarks.css b/apps/frontend/src/app/presenter/controller/bookmarks/index.css
similarity index 100%
rename from apps/frontend/src/features/controller/Bookmarks.css
rename to apps/frontend/src/app/presenter/controller/bookmarks/index.css
diff --git a/apps/frontend/src/features/controller/Bookmarks.tsx b/apps/frontend/src/app/presenter/controller/bookmarks/index.tsx
similarity index 80%
rename from apps/frontend/src/features/controller/Bookmarks.tsx
rename to apps/frontend/src/app/presenter/controller/bookmarks/index.tsx
index 03cfa3f8..5356b367 100644
--- a/apps/frontend/src/features/controller/Bookmarks.tsx
+++ b/apps/frontend/src/app/presenter/controller/bookmarks/index.tsx
@@ -1,6 +1,7 @@
-import './Bookmarks.css'
+import './index.css'
import { List, ListItem } from '@mui/material'
+import { createFileRoute } from '@tanstack/react-router'
import { useContext } from 'react'
import { withNavigationHotkeys } from '~/components/NavigationHotkeys'
@@ -33,7 +34,9 @@ const Bookmarks = ( { register, focused = 0 }: BookmarkProps ) => {
)
}
-export default withNavigationHotkeys( {
- arrowKeys: true,
- lineKeys: true,
-} )( Bookmarks )
+export const Route = createFileRoute( '/presenter/controller/bookmarks/' )( {
+ component: withNavigationHotkeys( {
+ arrowKeys: true,
+ lineKeys: true,
+ } )( Bookmarks ),
+} )
diff --git a/apps/frontend/src/features/controller/History.css b/apps/frontend/src/app/presenter/controller/history/index.css
similarity index 100%
rename from apps/frontend/src/features/controller/History.css
rename to apps/frontend/src/app/presenter/controller/history/index.css
diff --git a/apps/frontend/src/features/controller/History.tsx b/apps/frontend/src/app/presenter/controller/history/index.tsx
similarity index 91%
rename from apps/frontend/src/features/controller/History.tsx
rename to apps/frontend/src/app/presenter/controller/history/index.tsx
index 0330fa30..dd004356 100644
--- a/apps/frontend/src/features/controller/History.tsx
+++ b/apps/frontend/src/app/presenter/controller/history/index.tsx
@@ -1,4 +1,4 @@
-import './History.css'
+import './index.css'
import {
faDownload,
@@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemIcon from '@mui/material/ListItemIcon'
+import { createFileRoute } from '@tanstack/react-router'
import { stripVishraams } from 'gurmukhi-utils'
import { useContext, useMemo } from 'react'
@@ -78,7 +79,9 @@ const History = ( { register, focused = 0 }: HistoryProps ) => {
), [ register, focused, transitionHistory, latestLines ] )
}
-export default withNavigationHotkeys( {
- arrowKeys: true,
- lineKeys: true,
-} )( History )
+export const Route = createFileRoute( '/presenter/controller/history/' )( {
+ component: withNavigationHotkeys( {
+ arrowKeys: true,
+ lineKeys: true,
+ } )( History ),
+} )
diff --git a/apps/frontend/src/app/presenter/controller/index.tsx b/apps/frontend/src/app/presenter/controller/index.tsx
new file mode 100644
index 00000000..35231b2c
--- /dev/null
+++ b/apps/frontend/src/app/presenter/controller/index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute, Navigate } from '@tanstack/react-router'
+
+import { useControllerLocationHistory } from '../-controller-location-history'
+
+const Redirect = () => {
+ const lastControllerUrl = useControllerLocationHistory()
+
+ return
+}
+
+export const Route = createFileRoute( '/presenter/controller/' )( {
+ component: Redirect,
+} )
diff --git a/apps/frontend/src/features/controller/ShabadInfo.css b/apps/frontend/src/app/presenter/controller/navigator/-components/ShabadInfo/index.css
similarity index 100%
rename from apps/frontend/src/features/controller/ShabadInfo.css
rename to apps/frontend/src/app/presenter/controller/navigator/-components/ShabadInfo/index.css
diff --git a/apps/frontend/src/features/controller/ShabadInfo.tsx b/apps/frontend/src/app/presenter/controller/navigator/-components/ShabadInfo/index.tsx
similarity index 99%
rename from apps/frontend/src/features/controller/ShabadInfo.tsx
rename to apps/frontend/src/app/presenter/controller/navigator/-components/ShabadInfo/index.tsx
index e009a9cf..19b5e57a 100644
--- a/apps/frontend/src/features/controller/ShabadInfo.tsx
+++ b/apps/frontend/src/app/presenter/controller/navigator/-components/ShabadInfo/index.tsx
@@ -1,4 +1,4 @@
-import './ShabadInfo.css'
+import './index.css'
import { faInfoCircle, faTimesCircle } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
diff --git a/apps/frontend/src/features/controller/Navigator.css b/apps/frontend/src/app/presenter/controller/navigator/index.css
similarity index 100%
rename from apps/frontend/src/features/controller/Navigator.css
rename to apps/frontend/src/app/presenter/controller/navigator/index.css
diff --git a/apps/frontend/src/features/controller/Navigator.tsx b/apps/frontend/src/app/presenter/controller/navigator/index.tsx
similarity index 91%
rename from apps/frontend/src/features/controller/Navigator.tsx
rename to apps/frontend/src/app/presenter/controller/navigator/index.tsx
index c74d5906..8d11ea16 100644
--- a/apps/frontend/src/features/controller/Navigator.tsx
+++ b/apps/frontend/src/app/presenter/controller/navigator/index.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react/no-multi-comp */
-import './Navigator.css'
+import './index.css'
import {
faAngleDoubleLeft,
@@ -13,25 +13,24 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
+import { createFileRoute, useLocation } from '@tanstack/react-router'
import classNames from 'classnames'
import { stripVishraams } from 'gurmukhi-utils'
import { invert } from 'lodash'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
-import { Navigate, useLocation } from 'react-router-dom'
import GlobalHotKeys from '~/components/GlobalHotKeys'
import { withNavigationHotkeys } from '~/components/NavigationHotkeys'
import NavigatorHotKeys from '~/components/NavigatorHotkeys'
import { getJumpLines, getNextJumpLine } from '~/helpers/auto-jump'
-import { SEARCH_URL } from '~/helpers/consts'
import { ContentContext, HistoryContext } from '~/helpers/contexts'
import { LINE_HOTKEYS } from '~/helpers/keyMap'
import { findLineIndex } from '~/helpers/line'
import { useCurrentLines } from '~/hooks'
import controller from '~/services/controller'
-import ShabadInfo from './ShabadInfo'
-import ToolbarButton from './ToolbarButton'
+import ToolbarButton from '../-components/ToolbarButton'
+import ShabadInfo from './-components/ShabadInfo'
type NavigatorLineProps = {
register: ( id, line, ) => any,
@@ -44,12 +43,6 @@ type NavigatorLineProps = {
timestamp?: string | null,
}
-/**
-* Line component that attaches click handlers.
-* @param gurmukhi The Gurmukhi for the line to render.
-* @param id The id of the line.
-* @param index The index of the line.
-*/
const NavigatorLine = ( {
id,
register,
@@ -100,15 +93,7 @@ type NavigatorProps = {
focused?: string,
}
-/**
- * Navigator Component.
- * Displays lines from Shabad and allows navigation.
- */
-const Navigator = ( {
- updateFocus, register, focused,
-}: NavigatorProps ) => {
- const location = useLocation()
-
+const Navigator = ( { updateFocus, register, focused }: NavigatorProps ) => {
const { viewedLines } = useContext( HistoryContext )
const content = useContext( ContentContext )
@@ -138,7 +123,7 @@ const Navigator = ( {
} ), {} ), [] )
// If there's no Shabad to show, go back to the controller
- if ( !lines.length ) return
+ // if ( !lines.length ) return
const jumpLines = invert( getJumpLines( content ) )
const nextLineId = getNextJumpLine( content )
@@ -181,8 +166,6 @@ const NavigatorWithAllHotKeys = ( props ) => (
)
-export default NavigatorWithAllHotKeys
-
type BarProps = {
onHover: ( text: string | null ) => Record,
}
@@ -289,3 +272,8 @@ export const Bar = ( { onHover }: BarProps ) => {
)
}
+
+export const Route = createFileRoute( '/presenter/controller/navigator/' )( {
+ component: NavigatorWithAllHotKeys,
+
+} )
diff --git a/apps/frontend/src/features/controller/index.css b/apps/frontend/src/app/presenter/controller/route.css
similarity index 100%
rename from apps/frontend/src/features/controller/index.css
rename to apps/frontend/src/app/presenter/controller/route.css
diff --git a/apps/frontend/src/app/presenter/controller/route.tsx b/apps/frontend/src/app/presenter/controller/route.tsx
new file mode 100644
index 00000000..d017a116
--- /dev/null
+++ b/apps/frontend/src/app/presenter/controller/route.tsx
@@ -0,0 +1,219 @@
+import './route.css'
+
+import { faStar } from '@fortawesome/free-regular-svg-icons'
+import {
+ faCog,
+ faHistory,
+ faMap,
+ faSearch,
+ faSignOutAlt,
+ faVideoSlash,
+ faWindowMaximize,
+ faWindowMinimize,
+} from '@fortawesome/free-solid-svg-icons'
+import Toolbar from '@mui/material/Toolbar'
+import Typography from '@mui/material/Typography'
+import { createFileRoute, Outlet, ToPathOption, useLocation, useNavigate, useRouter } from '@tanstack/react-router'
+import { zodSearchValidator } from '@tanstack/router-zod-adapter'
+import classNames from 'classnames'
+import { useContext, useEffect, useState } from 'react'
+import { z } from 'zod'
+
+import { ContentContext, SettingsContext } from '~/helpers/contexts'
+import { useCurrentLines, usePrevious } from '~/hooks'
+import { useNavigateUtils } from '~/hooks/navigate'
+import controller from '~/services/controller'
+
+import ToolbarButton from './-components/ToolbarButton'
+
+type OnHover = ( message: string | null ) => Record
+
+type TopBarProps = {
+ title?: string,
+ onHover?: OnHover,
+}
+
+const TopBar = ( { title = '', onHover = () => ( {} ) }: TopBarProps ) => {
+ const resetHover = () => onHover( null )
+
+ const router = useRouter()
+ const navigate = Route.useNavigate()
+ const { open } = useNavigateUtils()
+ const { controllerOnly } = Route.useSearch()
+
+ return (
+
+ open( { to: '/settings' } )}
+ onMouseEnter={() => onHover( 'Settings' )}
+ onMouseLeave={resetHover}
+ />
+
+ {title}
+
+ void navigate( { to: '/' } )}
+ onMouseEnter={() => onHover( 'Hide Controller' )}
+ onMouseLeave={resetHover}
+ />
+
+ {controllerOnly ? (
+ void navigate( {
+ search: ( s ) => ( { ...s, controllerOnly: undefined } ),
+ } )}
+ onMouseEnter={() => onHover( 'Minimize Controller' )}
+ onMouseLeave={resetHover}
+ />
+ ) : (
+ void navigate( {
+ search: ( s ) => ( { ...s, controllerOnly: true } ),
+ } )}
+ onMouseEnter={() => onHover( 'Maximize Controller' )}
+ onMouseLeave={resetHover}
+ />
+ )}
+
+ {
+ const { href } = router.buildLocation( {
+ search: ( s ) => ( { ...s, controllerOnly: true } ),
+ } )
+
+ window.open( href, '_blank' )
+ }}
+ onMouseEnter={() => onHover( 'Pop Out Controller' )}
+ onMouseLeave={resetHover}
+ />
+
+ )
+}
+
+type BottomBarProps = {
+ onHover?: OnHover,
+ renderContent?: () => any,
+}
+
+const BottomBar = ( { renderContent = () => null, onHover = () => ( {} ) }: BottomBarProps ) => {
+ const navigate = useNavigate()
+
+ const lines = useCurrentLines()
+
+ const resetHover = () => onHover( null )
+
+ return (
+
+ void navigate( { to: '/presenter/controller/search', search: ( s ) => s } )} onHover={onHover} />
+
+ void navigate( { to: '/presenter/controller/history', search: ( s ) => s } )}
+ onMouseEnter={() => onHover( 'History' )}
+ onMouseLeave={resetHover}
+ />
+
+ void navigate( { to: '/presenter/controller/bookmarks', search: ( s ) => s } )}
+ onMouseEnter={() => onHover( 'Bookmarks' )}
+ onMouseLeave={resetHover}
+ />
+
+ {renderContent()}
+
+ {!!lines.length && (
+ void navigate( { to: '/presenter/controller/navigator', search: ( s ) => s } )}
+ onMouseEnter={() => onHover( 'Navigator' )}
+ onMouseLeave={resetHover}
+ />
+ )}
+
+ onHover( 'Clear' )}
+ onMouseLeave={resetHover}
+ />
+
+ )
+}
+
+const Controller = ( props ) => {
+ const { shabad, bani } = useContext( ContentContext )
+ const lines = useCurrentLines()
+
+ const previousLines = usePrevious( lines )
+
+ const [ hovered, setHovered ] = useState( null )
+
+ const pathname = useLocation( { select: ( l ) => l.pathname } )
+ const navigate = Route.useNavigate()
+
+ useEffect( () => {
+ const redirects = [
+ '/presenter/controller/search',
+ '/presenter/controller/history',
+ '/presenter/controller/bookmarks',
+ ] satisfies ToPathOption['to'][]
+
+ // Redirect to navigator tab if on one of the redirectable pages
+ const isTransition = lines.length && lines !== previousLines
+
+ if ( isTransition && redirects.some( ( route ) => pathname.includes( route ) ) ) {
+ void navigate( { to: '/presenter/controller/navigator' } )
+ }
+ }, [ history, lines, previousLines, navigate, pathname ] )
+
+ const settings = useContext( SettingsContext )
+ const { local: { theme: { simpleGraphics: simple } } } = settings
+
+ return (
+
+
+
+
+
+
+
+
BarComponent && (
+ //
+ // )}
+ />
+
+
+ )
+}
+
+export const Route = createFileRoute( '/presenter/controller' )( {
+ component: Controller,
+ validateSearch: zodSearchValidator( z.object( {
+ query: z.string().optional(),
+ } ) ),
+} )
diff --git a/apps/frontend/src/features/controller/Search/Result.tsx b/apps/frontend/src/app/presenter/controller/search/-components/Result.tsx
similarity index 84%
rename from apps/frontend/src/features/controller/Search/Result.tsx
rename to apps/frontend/src/app/presenter/controller/search/-components/Result.tsx
index 1ff312b2..00325c21 100644
--- a/apps/frontend/src/features/controller/Search/Result.tsx
+++ b/apps/frontend/src/app/presenter/controller/search/-components/Result.tsx
@@ -20,20 +20,7 @@ type ResultProps = {
translations: Record[],
}
-/**
- * Renders a single result, highlighting the match.
- * @param {string} gurmukhi The shabad line to display.
- * @param {int} typeId The type id of line.
- * @param {string} lineId The id of the line.
- * @param {string} shabadId The id of the shabad.
- * @param {int} sourceId The id of source.
- * @param {Object} shabad The object containing section information and other metadata.
- * @param {Boolean} focused Whether the line is focused or not.
- * @param {Function} highlighter The match highlighter.
- * @param {int} sourcePage The page number of shabad in source.
- * @param {string} translations The translations of shabad line to display.
- * @param {string} transliterations The transliterations of shabad line to display.
- */
+
const Result = forwardRef( ( {
gurmukhi,
typeId,
diff --git a/apps/frontend/src/features/controller/Search/match-highlighter.ts b/apps/frontend/src/app/presenter/controller/search/-match-highlighter.ts
similarity index 73%
rename from apps/frontend/src/features/controller/Search/match-highlighter.ts
rename to apps/frontend/src/app/presenter/controller/search/-match-highlighter.ts
index 09c3c243..b391e8ef 100644
--- a/apps/frontend/src/features/controller/Search/match-highlighter.ts
+++ b/apps/frontend/src/app/presenter/controller/search/-match-highlighter.ts
@@ -7,12 +7,6 @@ type MatchOptions = {
target: string,
}
-/**
- * Highlights a full word query against a matched line.
- * Finds the position to highlight in the target string using the Gurmukhi matched line,
- * and using the position of the highlighted words, highlights the same words in the target
- * string.
- */
const fullWordMatches = ( query: string ) => ( { target, gurmukhi }: MatchOptions ) => {
// Remove vishraams to prevent query from not matching
const baseGurmukhi = stripVishraams( gurmukhi )
@@ -47,11 +41,6 @@ const fullWordMatches = ( query: string ) => ( { target, gurmukhi }: MatchOption
]
}
-/**
- * Highlights a first letter query against a matched line.
- * Finds the words to match in the Gurmukhi string, and highlights
- * the corresponding target string.
- */
const firstLetterMatches = ( query: string ) => ( { target, gurmukhi }: MatchOptions ) => {
// Remove vishraams to prevent query from not matching
const baseGurmukhi = stripVishraams( gurmukhi )
@@ -73,12 +62,6 @@ const firstLetterMatches = ( query: string ) => ( { target, gurmukhi }: MatchOpt
]
}
-/**
- * Supported search mode match highlighters.
- * Highlighters must support highlighting against a 1-1 transliteration or gurmukhi string.
- * Highlighters all receive the same parameters.
- * Highlights must return a tuple of [ beforeMatch, match, afterMatch ]
- */
const highlighters = {
[ SEARCH_TYPES.fullWord ]: fullWordMatches,
[ SEARCH_TYPES.firstLetter ]: firstLetterMatches,
@@ -88,15 +71,6 @@ const UNDERSCORE_REGEX = /_/g
type SearchMode = ( typeof SEARCH_TYPES )[keyof typeof SEARCH_TYPES]
-/**
- * Separates the line into words before the first match, the first match, and after the match.
- * @param target The text to highlight.
- * @param context Contains gurmukhi and other contextual information required by all highlighters.
- * @param searchQuery The string inputted by the user.
- * @param searchMode The type of search being performed, either first word or full word.
- * @return An array of [ beforeMatch, match, afterMatch ],
- * with `match` being the highlighted section.`.
- */
const getHighlighter = ( searchQuery: string, searchMode: SearchMode ) => ( context: Omit ) => ( target: string ) => {
if ( !target ) return [ '', '', '' ]
diff --git a/apps/frontend/src/features/controller/Search/index.css b/apps/frontend/src/app/presenter/controller/search/index.css
similarity index 100%
rename from apps/frontend/src/features/controller/Search/index.css
rename to apps/frontend/src/app/presenter/controller/search/index.css
diff --git a/apps/frontend/src/features/controller/Search/index.tsx b/apps/frontend/src/app/presenter/controller/search/index.tsx
similarity index 79%
rename from apps/frontend/src/features/controller/Search/index.tsx
rename to apps/frontend/src/app/presenter/controller/search/index.tsx
index 2cdd3f0e..c6c8fb1b 100644
--- a/apps/frontend/src/features/controller/Search/index.tsx
+++ b/apps/frontend/src/app/presenter/controller/search/index.tsx
@@ -3,10 +3,9 @@ import './index.css'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconButton, Input, InputAdornment, List } from '@mui/material'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
import classNames from 'classnames'
-import { stringify } from 'qs'
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
-import { useLocation, useNavigate } from 'react-router-dom'
import { withNavigationHotkeys } from '~/components/NavigationHotkeys'
import {
@@ -16,27 +15,24 @@ import {
SEARCH_TYPES,
} from '~/helpers/consts'
import { SettingsContext } from '~/helpers/contexts'
-import { getUrlState } from '~/helpers/utils'
import controller from '~/services/controller'
-import getHighlighter from './match-highlighter'
-import Result from './Result'
+import Result from './-components/Result'
+import getHighlighter from './-match-highlighter'
// Generate the regex for capturing anchor chars, optionally
const searchRegex = new RegExp( `^([${Object.keys( SEARCH_ANCHORS ).map( ( anchor ) => `\\${anchor}` ).join( '' )}])?(.*)` )
-const getSearchParams = ( searchQuery ) => {
+const getSearchParams = ( searchQuery = '' ) => {
// Extract anchors and search query
- const [ , anchor, query ] = searchQuery.match( searchRegex )
-
- const inputValue = query
+ const [ , anchor, query ] = searchQuery.match( searchRegex ) ?? []
// Get search type from anchor char, if any
const type = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
const value = type === SEARCH_TYPES.firstLetter
- ? inputValue.slice().replace( new RegExp( SEARCH_CHARS.wildcard, 'g' ), '_' )
- : inputValue
+ ? query.slice().replace( new RegExp( SEARCH_CHARS.wildcard, 'g' ), '_' )
+ : query
return { anchor, value, type }
}
@@ -47,11 +43,6 @@ type SearchProps = {
focused?: string | number,
}
-/**
- * Search Component.
- * Converts ASCII to unicode on input.
- * Displays results.
- */
const Search = ( { updateFocus, register, focused }: SearchProps ) => {
const { local: {
search: {
@@ -63,8 +54,7 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => {
// Set the initial search query from URL
const navigate = useNavigate()
- const { search } = useLocation()
- const { query = '' } = getUrlState( search )
+ const { query } = Route.useSearch()
const [ searchedValue, setSearchedValue ] = useState( '' )
@@ -78,22 +68,13 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => {
const inputRef = useRef( null )
- /**
- * Set the received results and update the searched vale.
- * @param {Object[]} results An array of the returned results.
- */
const onResults = useCallback( ( results ) => {
setSearchedValue( inputValue.current )
setResults( results )
updateFocus( 0 )
}, [ updateFocus ] )
- /**
- * Run on change of value in the search box.
- * Converts ascii to unicode if need be.
- * Sends the search through to the controller.
- * @param {string} value The new value of the search box.
- */
+
const onChange = useCallback( ( { target: { value } } ) => {
const { anchor, type: searchType, value: searchValue } = getSearchParams( value )
@@ -112,13 +93,15 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => {
setAnchor( anchor )
// Update URL with search
- navigate( { search: `?${stringify( {
- ...getUrlState( search ),
- query: value,
- } )}` } )
+ void navigate( {
+ search: ( s ) => ( {
+ ...s,
+ query: value,
+ } ),
+ replace: true,
+ } )
}, [
navigate,
- search,
resultTranslationLanguage,
resultTransliterationLanguage,
showResultCitations,
@@ -205,11 +188,13 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => {
)
}
-export default withNavigationHotkeys( {
- keymap: {
- next: [ 'down', 'tab' ],
- previous: [ 'up', 'shift+tab' ],
- first: null,
- last: null,
- },
-} )( Search )
+export const Route = createFileRoute( '/presenter/controller/search/' )( {
+ component: withNavigationHotkeys( {
+ keymap: {
+ next: [ 'down', 'tab' ],
+ previous: [ 'up', 'shift+tab' ],
+ first: null,
+ last: null,
+ },
+ } )( Search ),
+} )
diff --git a/apps/frontend/src/features/presenter/index.css b/apps/frontend/src/app/presenter/route.css
similarity index 100%
rename from apps/frontend/src/features/presenter/index.css
rename to apps/frontend/src/app/presenter/route.css
diff --git a/apps/frontend/src/features/presenter/index.tsx b/apps/frontend/src/app/presenter/route.lazy.tsx
similarity index 52%
rename from apps/frontend/src/features/presenter/index.tsx
rename to apps/frontend/src/app/presenter/route.lazy.tsx
index e7054162..719756a9 100644
--- a/apps/frontend/src/features/presenter/index.tsx
+++ b/apps/frontend/src/app/presenter/route.lazy.tsx
@@ -1,14 +1,13 @@
-import './index.css'
+import './route.css'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import CssBaseline from '@mui/material/CssBaseline'
+import { CssBaseline } from '@mui/material'
import IconButton from '@mui/material/IconButton'
+import { createLazyFileRoute, Outlet, useLocation } from '@tanstack/react-router'
import classNames from 'classnames'
-import queryString from 'qs'
import { lazy, Suspense, useContext, useRef, useState } from 'react'
import { EventsType, useIdleTimer } from 'react-idle-timer'
-import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import CopyHotkeys from '~/components/CopyHotkeys'
import { withErrorFallback } from '~/components/ErrorFallback'
@@ -16,29 +15,20 @@ import GlobalHotKeys from '~/components/GlobalHotKeys'
import Loader from '~/components/Loader'
import NavigatorHotKeys from '~/components/NavigatorHotkeys'
import ThemeLoader from '~/components/ThemeLoader'
-import {
- BOOKMARKS_URL,
- CONTROLLER_URL,
- HISTORY_URL,
- isDesktop,
- isMobile,
- NAVIGATOR_URL,
- SEARCH_URL,
- SETTINGS_URL,
- STATES,
-} from '~/helpers/consts'
+import { isDesktop } from '~/helpers/consts'
import { SettingsContext } from '~/helpers/contexts'
import { toggleFullscreen } from '~/helpers/electron-utils'
import { GLOBAL_SHORTCUTS } from '~/helpers/keyMap'
-import { OPTIONS } from '~/helpers/options'
-import { getUrlState } from '~/helpers/utils'
-import { useCurrentLines, useMount } from '~/hooks'
+import { CLIENT_OPTIONS } from '~/helpers/options'
+import { useCurrentLines } from '~/hooks'
+import { useNavigateUtils } from '~/hooks/navigate'
import controller from '~/services/controller'
-import StatusToast from './StatusToast'
+import StatusToast from './-components/StatusToast'
+import { ControllerLocationHistoryProvider } from './-controller-location-history'
+import { Route as PresenterRoute } from './route'
-const Display = lazy( () => import( './Display' ) )
-const Controller = lazy( () => import( '~/features/controller' ) )
+const Display = lazy( () => import( './-components/Display' ) )
export const IDLE_TIMEOUT = 1000 * 3
const IDLE_EVENTS = [
@@ -54,10 +44,12 @@ const IDLE_EVENTS = [
] as const satisfies EventsType[]
const Presenter = () => {
- const navigate = useNavigate()
- const location = useLocation()
- const { search, pathname } = location
- const { controllerOnly } = getUrlState( search )
+ const { open } = useNavigateUtils()
+ const navigate = PresenterRoute.useNavigate()
+ const pathname = useLocation( { select: ( l ) => l.pathname } )
+ const { controllerOnly } = PresenterRoute.useSearch()
+
+ const isControllerOpen = pathname.includes( '/presenter/controller' )
const [ isIdle, setIsIdle ] = useState( false )
@@ -71,8 +63,6 @@ const Presenter = () => {
const lines = useCurrentLines()
- const isControllerOpen = pathname.includes( CONTROLLER_URL )
-
const { local: localSettings } = useContext( SettingsContext )
const {
theme: { themeName },
@@ -80,70 +70,24 @@ const Presenter = () => {
hotkeys,
} = localSettings
- /**
- * Sets the query string parameters, retaining any currently present.
- * @param params The query string parameters.
- */
- const setQueryParams = ( params ) => navigate( {
- ...location,
- search: queryString.stringify( { ...getUrlState( search ), ...params } ),
- } )
+ const toggleController = () => void navigate( { to: isControllerOpen ? '/' : '/presenter/controller' } )
- /**
- * More concise form to navigate to URLs, retaining query params.
- * @param pathname The path to navigate to.
- */
- const go = ( pathname ) => navigate( { ...location, pathname } )
-
- /**
- * Toggles the controller.
- */
- const toggleController = () => {
- const nextURL = pathname.includes( CONTROLLER_URL ) ? '/' : CONTROLLER_URL
- go( nextURL )
- }
-
- /**
- * Always puts the controller in fullscreen.
- */
- const setFullscreenController = () => navigate( {
- pathname: CONTROLLER_URL,
- search: queryString.stringify( { [ STATES.controllerOnly ]: true } ),
+ const { controllerZoom } = CLIENT_OPTIONS
+ const setZoom = ( controllerZoom: number ) => controller.setSettings( {
+ layout: { controllerZoom },
} )
- const { controllerZoom } = OPTIONS
- const setZoom = ( controllerZoom ) => controller.setSettings( { layout: { controllerZoom } } )
const zoomInController = () => setZoom( Math.min( controllerZoom.max, zoom + 0.1 ) )
const zoomOutController = () => setZoom( Math.max( controllerZoom.min, zoom - 0.1 ) )
const zoomResetController = () => setZoom( 1 )
- /**
- * Toggles the given query string parameter.
- * @param query The query string parameter to toggle.
- */
- const toggleQuery = ( query ) => {
- const parsed = getUrlState( search )
-
- setQueryParams( {
- ...parsed,
- [ query ]: parsed[ query ] ? undefined : true,
- } )
- }
-
- /**
- * Toggles the controller in fullscreen.
- */
const toggleFullscreenController = () => {
- // Navigates to the controller first, if not there
- if ( !pathname.includes( CONTROLLER_URL ) ) toggleController()
-
- toggleQuery( STATES.controllerOnly )
+ void navigate( {
+ ...( !pathname.includes( '/presenter/controller' ) && { pathname: '/presenter/controller' } ),
+ search: ( s ) => ( { controllerOnly: !s.controllerOnly } ),
+ } )
}
- /**
- * Prevents the default action from occurring for each handler.
- * @param events An object containing the event names and corresponding handlers.
- */
const preventDefault = ( events ) => Object.entries( events )
.reduce( ( events, [ name, handler ] ) => ( {
...events,
@@ -156,22 +100,24 @@ const Presenter = () => {
[ GLOBAL_SHORTCUTS.zoomOutController.name ]: zoomOutController,
[ GLOBAL_SHORTCUTS.zoomResetController.name ]: zoomResetController,
[ GLOBAL_SHORTCUTS.toggleController.name ]: toggleController,
- [ GLOBAL_SHORTCUTS.newController.name ]: () => controller.openWindow( `${CONTROLLER_URL}?${STATES.controllerOnly}=true`, { alwaysOnTop: true } ),
- [ GLOBAL_SHORTCUTS.settings.name ]: () => controller.openWindow( SETTINGS_URL ),
- [ GLOBAL_SHORTCUTS.search.name ]: () => go( SEARCH_URL ),
- [ GLOBAL_SHORTCUTS.history.name ]: () => go( HISTORY_URL ),
- [ GLOBAL_SHORTCUTS.bookmarks.name ]: () => go( BOOKMARKS_URL ),
- [ GLOBAL_SHORTCUTS.navigator.name ]: () => lines.length && go( NAVIGATOR_URL ),
+ [ GLOBAL_SHORTCUTS.newController.name ]: () => {
+ open( {
+ search: ( s ) => ( { ...s, controllerOnly: true } ),
+ } )
+ },
+ [ GLOBAL_SHORTCUTS.settings.name ]: () => {
+ open( { to: '/settings' } )
+ },
+ [ GLOBAL_SHORTCUTS.search.name ]: () => void navigate( { to: '/presenter/controller/search' } ),
+ [ GLOBAL_SHORTCUTS.history.name ]: () => void navigate( { to: '/presenter/controller/history' } ),
+ [ GLOBAL_SHORTCUTS.bookmarks.name ]: () => void navigate( { to: '/presenter/controller/bookmarks' } ),
+ [ GLOBAL_SHORTCUTS.navigator.name ]: () => lines.length && void navigate( { to: '/presenter/controller/navigator' } ),
[ GLOBAL_SHORTCUTS.clearDisplay.name ]: controller.clear,
[ GLOBAL_SHORTCUTS.toggleFullscreenController.name ]: toggleFullscreenController,
[ GLOBAL_SHORTCUTS.toggleFullscreen.name ]: toggleFullscreen,
[ GLOBAL_SHORTCUTS.quit.name ]: window.close,
} )
- useMount( () => {
- if ( isMobile ) setFullscreenController()
- } )
-
// Required for mouse shortcuts
const presenterRef = useRef( null )
@@ -193,12 +139,9 @@ const Presenter = () => {
-
- }
- />
-
+
+
+
@@ -210,4 +153,6 @@ const Presenter = () => {
)
}
-export default withErrorFallback( Presenter )
+export const Route = createLazyFileRoute( '/presenter' )( {
+ component: withErrorFallback( Presenter ),
+} )
diff --git a/apps/frontend/src/app/presenter/route.tsx b/apps/frontend/src/app/presenter/route.tsx
new file mode 100644
index 00000000..3e71a791
--- /dev/null
+++ b/apps/frontend/src/app/presenter/route.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { zodSearchValidator } from '@tanstack/router-zod-adapter'
+import { z } from 'zod'
+
+import { isMobile } from '~/helpers/consts'
+
+export const Route = createFileRoute( '/presenter' )( {
+ beforeLoad: () => {
+ if ( isMobile ) {
+ redirect( {
+ throw: true,
+ to: '/presenter/controller',
+ search: {
+ controllerOnly: true,
+ },
+ } )
+ }
+ },
+ validateSearch: zodSearchValidator( z.object( {
+ controllerOnly: z.boolean().optional(),
+ } ) ),
+} )
diff --git a/apps/frontend/src/features/screen-reader/index.css b/apps/frontend/src/app/screen-reader/index.css
similarity index 100%
rename from apps/frontend/src/features/screen-reader/index.css
rename to apps/frontend/src/app/screen-reader/index.css
diff --git a/apps/frontend/src/features/screen-reader/index.tsx b/apps/frontend/src/app/screen-reader/index.lazy.tsx
similarity index 92%
rename from apps/frontend/src/features/screen-reader/index.tsx
rename to apps/frontend/src/app/screen-reader/index.lazy.tsx
index 7aa648ab..685065d2 100644
--- a/apps/frontend/src/features/screen-reader/index.tsx
+++ b/apps/frontend/src/app/screen-reader/index.lazy.tsx
@@ -1,5 +1,6 @@
import './index.css'
+import { createLazyFileRoute } from '@tanstack/react-router'
import classNames from 'classnames'
import { useContext } from 'react'
@@ -80,4 +81,6 @@ const ScreenReader = () => {
)
}
-export default ScreenReader
+export const Route = createLazyFileRoute( '/screen-reader/' )( {
+ component: ScreenReader,
+} )
diff --git a/apps/frontend/src/features/settings/CopyButton.css b/apps/frontend/src/app/settings/-components/CopyButton/index.css
similarity index 100%
rename from apps/frontend/src/features/settings/CopyButton.css
rename to apps/frontend/src/app/settings/-components/CopyButton/index.css
diff --git a/apps/frontend/src/features/settings/CopyButton.tsx b/apps/frontend/src/app/settings/-components/CopyButton/index.tsx
similarity index 96%
rename from apps/frontend/src/features/settings/CopyButton.tsx
rename to apps/frontend/src/app/settings/-components/CopyButton/index.tsx
index e92569db..e80c480c 100644
--- a/apps/frontend/src/features/settings/CopyButton.tsx
+++ b/apps/frontend/src/app/settings/-components/CopyButton/index.tsx
@@ -1,4 +1,4 @@
-import './CopyButton.css'
+import './index.css'
import { Button, Tooltip } from '@mui/material'
diff --git a/apps/frontend/src/features/settings/DynamicOptions.tsx b/apps/frontend/src/app/settings/-components/DynamicOptions.tsx
similarity index 86%
rename from apps/frontend/src/features/settings/DynamicOptions.tsx
rename to apps/frontend/src/app/settings/-components/DynamicOptions.tsx
index bdc49b67..23ba881e 100644
--- a/apps/frontend/src/features/settings/DynamicOptions.tsx
+++ b/apps/frontend/src/app/settings/-components/DynamicOptions.tsx
@@ -5,10 +5,10 @@ import { ClientSettings, Settings } from '@presenter/contract'
import { useContext } from 'react'
import { SettingsContext } from '~/helpers/contexts'
-import { DEFAULT_OPTIONS, FLAT_OPTION_GROUPS, OPTIONS, PRIVACY_TYPES } from '~/helpers/options'
+import { CLIENT_OPTIONS, DEFAULT_OPTIONS, FLAT_OPTION_GROUPS } from '~/helpers/options'
import controller from '~/services/controller'
-import SettingComponentFactory, { Button } from './SettingComponents'
+import SettingComponentFactory, { Button } from './SettingsComponents'
export const slotSizes = {
icon: { xs: 2, sm: 1 },
@@ -71,7 +71,6 @@ export const ResetButton = ( { group, disabled = false, device }: ResetButtonPro