From 10e49468baa484371c4982b2465985d22bc875df Mon Sep 17 00:00:00 2001 From: Harjot Singh Date: Tue, 5 Nov 2024 02:58:13 +0000 Subject: [PATCH] feat(frontend): add content hooks and typesafe websocket client --- apps/frontend/package.json | 4 +- apps/frontend/src/app/__root.tsx | 131 +--- .../app/overlay/-components/ThemeLoader.tsx | 6 +- apps/frontend/src/app/overlay/index.lazy.tsx | 5 +- .../presenter/-components/Display/index.tsx | 11 +- .../-components/StatusToast/index.tsx | 6 +- .../presenter/controller/bookmarks/index.tsx | 11 +- .../presenter/controller/history/index.tsx | 5 +- .../-components/ShabadInfo/index.tsx | 15 +- .../presenter/controller/navigator/index.tsx | 75 +- .../src/app/presenter/controller/route.tsx | 17 +- .../controller/search/-components/Result.tsx | 4 +- .../app/presenter/controller/search/index.tsx | 27 +- .../frontend/src/app/presenter/route.lazy.tsx | 14 +- .../src/app/screen-reader/index.lazy.tsx | 8 +- apps/frontend/src/components/CopyHotkeys.tsx | 18 +- .../src/components/NavigatorHotkeys.tsx | 27 +- apps/frontend/src/components/ThemeLoader.tsx | 7 +- apps/frontend/src/helpers/auto-jump.ts | 28 +- apps/frontend/src/helpers/consts.ts | 1 - apps/frontend/src/helpers/contexts.tsx | 25 - apps/frontend/src/helpers/line.ts | 24 +- apps/frontend/src/hooks/index.ts | 30 +- apps/frontend/src/index.tsx | 4 +- apps/frontend/src/routeTree.gen.ts | 229 +++++- apps/frontend/src/services/bookmarks.ts | 5 + apps/frontend/src/services/content.ts | 36 + apps/frontend/src/services/controller.ts | 21 +- apps/frontend/src/services/jotai.ts | 3 + apps/frontend/src/services/notifications.ts | 9 + apps/frontend/src/services/search.ts | 15 + apps/frontend/src/services/status.ts | 30 + apps/frontend/src/services/tracker.ts | 10 + .../frontend/src/services/websocket-client.ts | 96 +++ package-lock.json | 717 ++++++++++-------- 35 files changed, 1017 insertions(+), 657 deletions(-) create mode 100644 apps/frontend/src/services/bookmarks.ts create mode 100644 apps/frontend/src/services/content.ts create mode 100644 apps/frontend/src/services/jotai.ts create mode 100644 apps/frontend/src/services/notifications.ts create mode 100644 apps/frontend/src/services/search.ts create mode 100644 apps/frontend/src/services/status.ts create mode 100644 apps/frontend/src/services/tracker.ts create mode 100644 apps/frontend/src/services/websocket-client.ts diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 3f43664d..712e0b58 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -29,6 +29,7 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@mui/material": "^5.16.6", + "@presenter/swiss-knife": "*", "@presenter/themes": "*", "@sentry/browser": "^6.19.7", "@tanstack/react-router": "^1.49.2", @@ -39,15 +40,16 @@ "eventemitter3": "^5.0.1", "gurmukhi-utils": "^3.2.2", "is-mobile": "^3.1.1", + "jotai": "^2.10.1", "lodash": "^4.17.21", "memoizee": "^0.4.17", "notistack": "^3.0.1", + "partysocket": "^1.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hotkeys": "^2.0.0", "react-idle-timer": "^5.7.2", "react-transition-group": "^4.4.5", - "reconnecting-websocket": "^4.4.0", "scroll-into-view": "^1.16.0", "zod": "^3.8.2-alpha.6" }, diff --git a/apps/frontend/src/app/__root.tsx b/apps/frontend/src/app/__root.tsx index c796ed73..0fba55b1 100644 --- a/apps/frontend/src/app/__root.tsx +++ b/apps/frontend/src/app/__root.tsx @@ -1,63 +1,45 @@ import './__root.css' -import { RecommendedSources, Shabad, Writer } from '@presenter/contract' +import { RecommendedSources, Writer } from '@presenter/contract' import { createRootRoute, Navigate, Outlet } from '@tanstack/react-router' import classNames from 'classnames' +import { Provider } from 'jotai' import { SnackbarProvider } from 'notistack' import { PureComponent, Suspense } from 'react' import Loader from '~/components/Loader' import { API_URL, isDesktop, isMobile, isTablet } from '~/helpers/consts' import { - BookmarksContext, - ContentContext, HistoryContext, RecommendedSourcesContext, SettingsContext, - StatusContext, WritersContext, } from '~/helpers/contexts' import { DEFAULT_OPTIONS } from '~/helpers/options' import { merge } from '~/helpers/utils' -import controller from '~/services/controller' +import { store } from '~/services/jotai' +// import controller from '~/services/controller' -const loadSettings = () => merge( { local: controller.readSettings() }, DEFAULT_OPTIONS ) +// const loadSettings = () => merge( { local: controller.readSettings() }, DEFAULT_OPTIONS ) +const loadSettings = () => DEFAULT_OPTIONS class App extends PureComponent { state = { - connected: false, - connectedAt: null, - status: null, - banis: [], - bani: null, - lineId: null, - mainLineId: null, - nextLineId: null, viewedLines: {}, transitionHistory: {}, latestLines: {}, - shabad: null, recommendedSources: {} as RecommendedSources['recommendedSources'], writers: {}, settings: loadSettings(), - next: {}, } componentDidMount() { // Register controller event - controller.on( 'connected', this.onConnected ) - controller.on( 'disconnected', this.onDisconnected ) - controller.on( 'shabads:current', this.onShabad ) - controller.on( 'lines:current', this.onLine ) - controller.on( 'lines:main', this.onMainLine ) - controller.on( 'lines:next', this.onNextLine ) - controller.on( 'history:viewed-lines', this.onViewedLines ) - controller.on( 'history:transitions', this.onTransitionHistory ) - controller.on( 'history:latest-lines', this.onLatestLineHistory ) - controller.on( 'banis:list', this.onBanis ) - controller.on( 'banis:current', this.onBani ) - controller.on( 'status', this.onStatus ) - controller.on( 'settings', this.onSettings ) + // controller.on( 'history:viewed-lines', this.onViewedLines ) + // controller.on( 'history:transitions', this.onTransitionHistory ) + // controller.on( 'history:latest-lines', this.onLatestLineHistory ) + // controller.on( 'banis:list', this.onBanis ) + // controller.on( 'settings', this.onSettings ) // Get recommended sources and set as settings, if there are none void fetch( `${API_URL}/sources` ) @@ -77,54 +59,22 @@ class App extends PureComponent { componentWillUnmount() { // Deregister event listeners from controller - controller.off( 'connected', this.onConnected ) - controller.off( 'disconnected', this.onDisconnected ) - controller.off( 'shabads:current', this.onShabad ) - controller.off( 'lines:current', this.onLine ) - controller.off( 'history:transitions', this.onTransitionHistory ) - controller.off( 'history:latest-lines', this.onLatestLineHistory ) - controller.off( 'lines:main', this.onMainLine ) - controller.off( 'lines:next', this.onNextLine ) - controller.off( 'lines:viewed', this.onViewedLines ) - controller.off( 'banis:list', this.onBanis ) - controller.off( 'banis:current', this.onBani ) - controller.off( 'status', this.onStatus ) - controller.off( 'settings', this.onSettings ) + // controller.off( 'shabads:current', this.onShabad ) + // controller.off( 'lines:current', this.onLine ) + // controller.off( 'history:transitions', this.onTransitionHistory ) + // controller.off( 'history:latest-lines', this.onLatestLineHistory ) + // controller.off( 'lines:main', this.onMainLine ) + // controller.off( 'lines:next', this.onNextLine ) + // controller.off( 'lines:viewed', this.onViewedLines ) + // controller.off( 'banis:list', this.onBanis ) + // controller.off( 'banis:current', this.onBani ) + // controller.off( 'settings', this.onSettings ) } - onConnected = () => this.setState( { - connectedAt: new Date(), - connected: true, - bani: null, - shabad: null, - } ) - - onDisconnected = () => this.setState( { connected: false } ) - - onShabad = ( shabad: Shabad ) => this.setState( { next: { shabad, bani: null } } ) - - onLine = ( lineId: string ) => this.setState( - ( { next }: any ) => ( { lineId, ...next, next: {} } ) - ) - - onViewedLines = ( viewedLines: any[] ) => this.setState( { viewedLines } ) - - onMainLine = ( mainLineId: string ) => this.setState( { mainLineId } ) - - onNextLine = ( nextLineId: string ) => this.setState( { nextLineId } ) - - onTransitionHistory = ( transitionHistory: any[] ) => this.setState( { transitionHistory } ) - - onLatestLineHistory = ( latestLines: any[] ) => this.setState( { latestLines } ) - - onStatus = ( status: any ) => this.setState( { status } ) - onBanis = ( banis: any[] ) => this.setState( { banis } ) - onBani = ( bani: any ) => this.setState( { next: { bani, shabad: null } } ) - onSettings = ( { global = {}, local = {}, ...settings } ) => { - controller.saveLocalSettings( local, false ) + // controller.saveLocalSettings( local, false ) this.setState( ( state: typeof this.state ) => ( { settings: { @@ -135,7 +85,7 @@ class App extends PureComponent { ...deviceSettings, [ host ]: merge( DEFAULT_OPTIONS.local, config ), } ), {} ), - local: controller.readSettings(), + // local: controller.readSettings(), global: merge( state.settings.global, global ), }, } ) ) @@ -143,17 +93,8 @@ class App extends PureComponent { render() { const { - connected, - connectedAt, - status, - banis, recommendedSources, writers, - bani, - shabad, - lineId, - mainLineId, - nextLineId, viewedLines, transitionHistory, latestLines, @@ -162,25 +103,21 @@ class App extends PureComponent { return (
- }> - + + }> - - - - - - - - - - - + + + + + + + - - + +
) } diff --git a/apps/frontend/src/app/overlay/-components/ThemeLoader.tsx b/apps/frontend/src/app/overlay/-components/ThemeLoader.tsx index 02d6aadb..9bb59cf7 100644 --- a/apps/frontend/src/app/overlay/-components/ThemeLoader.tsx +++ b/apps/frontend/src/app/overlay/-components/ThemeLoader.tsx @@ -1,14 +1,12 @@ -import { useContext } from 'react' - import { API_URL } from '~/helpers/consts' -import { StatusContext } from '~/helpers/contexts' +import { useStatus } from '~/services/status' const OVERLAY_THEMES_URL = `${API_URL}/themes/overlay` type ThemeLoaderProps = { name?: string } const ThemeLoader = ( { name }: ThemeLoaderProps ) => { - const { connectedAt } = useContext( StatusContext ) + const { connectedAt } = useStatus() return ( { const settings = useContext( SettingsContext ) - const { connected } = useContext( StatusContext ) + const { connected } = useStatus() const { global: globalSettings } = settings || {} const { overlay: { overlayName, ...overlay } } = globalSettings || {} diff --git a/apps/frontend/src/app/presenter/-components/Display/index.tsx b/apps/frontend/src/app/presenter/-components/Display/index.tsx index e0d84658..e5df2307 100644 --- a/apps/frontend/src/app/presenter/-components/Display/index.tsx +++ b/apps/frontend/src/app/presenter/-components/Display/index.tsx @@ -7,7 +7,8 @@ import { LANGUAGES } from '~/helpers/data' import { customiseLine, getTransliterators } from '~/helpers/line' import { ClientSettings } from '~/helpers/options' import { filterFalsyValues } from '~/helpers/utils' -import { useCurrentLine, useCurrentLines, useTranslations } from '~/hooks' +import { useTranslations } from '~/hooks' +import { useContent } from '~/services/content' import Line from '../Line' @@ -31,9 +32,7 @@ const Display = ( { settings }: DisplayProps ) => { const { lineEnding } = display // Find the correct line in the Shabad - const lines = useCurrentLines() - const [ line, lineIndex ] = useCurrentLine() - const typeId = ( line?.typeId ) || -1 + const { line, lineIndex, lines } = useContent() // Get the next lines const { nextLines: nextLineCount, previousLines: previousLineCount } = display @@ -48,7 +47,7 @@ const Display = ( { settings }: DisplayProps ) => { display.punjabiTranslation && LANGUAGES.punjabi, display.spanishTranslation && LANGUAGES.spanish, ] ) as number[] ), - ( line ) => customiseLine( line, { lineEnding, typeId } ), + ( line ) => customiseLine( line, { lineEnding, typeId: line.typeId } ), ) const transliterators = mapValues( @@ -58,7 +57,7 @@ const Display = ( { settings }: DisplayProps ) => { display.urduTransliteration && LANGUAGES.urdu, ] ) as number[] ), ( transliterate ) => ( text: string ) => transliterate( - customiseLine( text, { lineEnding, typeId } ) + customiseLine( text, { lineEnding, typeId: line?.typeId } ), ), ) diff --git a/apps/frontend/src/app/presenter/-components/StatusToast/index.tsx b/apps/frontend/src/app/presenter/-components/StatusToast/index.tsx index bf47f73f..8b8a00e2 100644 --- a/apps/frontend/src/app/presenter/-components/StatusToast/index.tsx +++ b/apps/frontend/src/app/presenter/-components/StatusToast/index.tsx @@ -2,13 +2,13 @@ import './index.css' import { faInfoCircle } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { useContext } from 'react' import { CSSTransition, TransitionGroup } from 'react-transition-group' -import { StatusContext } from '~/helpers/contexts' +import { useNotifications } from '~/services/notifications' const StatusToast = () => { - const { status } = useContext( StatusContext ) + const status = useNotifications() + return ( {status && ( diff --git a/apps/frontend/src/app/presenter/controller/bookmarks/index.tsx b/apps/frontend/src/app/presenter/controller/bookmarks/index.tsx index 5356b367..7541017f 100644 --- a/apps/frontend/src/app/presenter/controller/bookmarks/index.tsx +++ b/apps/frontend/src/app/presenter/controller/bookmarks/index.tsx @@ -2,12 +2,11 @@ import './index.css' import { List, ListItem } from '@mui/material' import { createFileRoute } from '@tanstack/react-router' -import { useContext } from 'react' import { withNavigationHotkeys } from '~/components/NavigationHotkeys' -import { BookmarksContext } from '~/helpers/contexts' import { LINE_HOTKEYS } from '~/helpers/keyMap' -import controller from '~/services/controller' +import { useBookmarks } from '~/services/bookmarks' +import { setContent } from '~/services/content' type BookmarkProps = { focused?: number, @@ -15,16 +14,16 @@ type BookmarkProps = { } const Bookmarks = ( { register, focused = 0 }: BookmarkProps ) => { - const bookmarks = useContext( BookmarksContext ) + const bookmarks = useBookmarks() return ( - {bookmarks.map( ( { id, nameGurmukhi }, index ) => ( + {bookmarks?.map( ( { id, nameGurmukhi }, index ) => ( register( index, ref )} - onClick={() => controller.bani( { baniId: id } )} + onClick={() => setContent( { id, type: 'bani' } )} > {LINE_HOTKEYS[ index ]} {nameGurmukhi} diff --git a/apps/frontend/src/app/presenter/controller/history/index.tsx b/apps/frontend/src/app/presenter/controller/history/index.tsx index dd004356..d391fd6e 100644 --- a/apps/frontend/src/app/presenter/controller/history/index.tsx +++ b/apps/frontend/src/app/presenter/controller/history/index.tsx @@ -16,6 +16,7 @@ import { withNavigationHotkeys } from '~/components/NavigationHotkeys' import { HISTORY_DOWNLOAD_URL } from '~/helpers/consts' import { HistoryContext } from '~/helpers/contexts' import { LINE_HOTKEYS } from '~/helpers/keyMap' +import { setContent } from '~/services/content' import controller from '~/services/controller' type HistoryProps = { @@ -43,8 +44,8 @@ const History = ( { register, focused = 0 }: HistoryProps ) => { const latestLineId = latestLine ? latestLine.id : lineId const onClick = () => ( bani - ? controller.bani( { baniId: bani.id, lineId: latestLineId } ) - : controller.shabad( { shabadId, lineId: latestLineId } ) ) + ? setContent( { type: 'bani', id: bani.id, lineId: latestLineId } ) + : setContent( { type: 'shabad', id: shabadId, lineId: latestLineId } ) ) return ( { const [ isPopoverOpen, setPopoverOpen ] = useState( false ) - const { shabad, bani } = useContext( ContentContext ) const writers = useContext( WritersContext ) const recommendedSources = useContext( RecommendedSourcesContext ) - const [ line ] = useCurrentLine() - const lines = useCurrentLines() - const onClick = () => setPopoverOpen( true ) const onClose = () => setPopoverOpen( false ) @@ -37,8 +34,10 @@ const ShabadInfo = () => { // Icon changes when open const barIcon = isPopoverOpen ? faTimesCircle : faInfoCircle - const { sourceId, writerId, section } = shabad || line.shabad - const { nameEnglish: sectionName } = bani || section + const { content, line } = useContent() + + const { sourceId, writerId, section } = content?.type === 'shabad' ? content.shabad : line?.shabad + const { nameEnglish: sectionName } = content?.type === 'bani' ? content.bani : section const { id: lineId, sourcePage } = line const { nameEnglish: writerName } = writers[ writerId ] diff --git a/apps/frontend/src/app/presenter/controller/navigator/index.tsx b/apps/frontend/src/app/presenter/controller/navigator/index.tsx index 8d11ea16..7cfbbffb 100644 --- a/apps/frontend/src/app/presenter/controller/navigator/index.tsx +++ b/apps/frontend/src/app/presenter/controller/navigator/index.tsx @@ -13,7 +13,7 @@ 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 { createFileRoute } from '@tanstack/react-router' import classNames from 'classnames' import { stripVishraams } from 'gurmukhi-utils' import { invert } from 'lodash' @@ -23,11 +23,11 @@ import GlobalHotKeys from '~/components/GlobalHotKeys' import { withNavigationHotkeys } from '~/components/NavigationHotkeys' import NavigatorHotKeys from '~/components/NavigatorHotkeys' import { getJumpLines, getNextJumpLine } from '~/helpers/auto-jump' -import { ContentContext, HistoryContext } from '~/helpers/contexts' +import { HistoryContext } from '~/helpers/contexts' import { LINE_HOTKEYS } from '~/helpers/keyMap' -import { findLineIndex } from '~/helpers/line' -import { useCurrentLines } from '~/hooks' +import { setLine, useContent } from '~/services/content' import controller from '~/services/controller' +import { useTracker } from '~/services/tracker' import ToolbarButton from '../-components/ToolbarButton' import ShabadInfo from './-components/ShabadInfo' @@ -54,7 +54,7 @@ const NavigatorLine = ( { timestamp = null, }: NavigatorLineProps ) => { // Move to the line id on click - const onClick = () => controller.line( id ) + const onClick = () => setLine( id ) // Register the reference to the line with the NavigationHotKey HOC const registerLine = ( line ) => register( id, line, true ) @@ -96,18 +96,16 @@ type NavigatorProps = { const Navigator = ( { updateFocus, register, focused }: NavigatorProps ) => { const { viewedLines } = useContext( HistoryContext ) - const content = useContext( ContentContext ) - const { shabad, bani, lineId, mainLineId } = content - - const lines = useCurrentLines() + const { content, lines, lineId } = useContent() + const { mainLineId } = useTracker() // Set the focus to the active line when it changes useEffect( () => { updateFocus( lineId, false ) }, [ lineId, updateFocus ] ) const goToIndex = useCallback( ( index ) => { - const jumpLines = getJumpLines( { shabad, bani } ) + const jumpLines = getJumpLines( content ) updateFocus( jumpLines[ index ] ) - }, [ updateFocus, shabad, bani ] ) + }, [ updateFocus, content ] ) // Navigation Hotkey Handlers const hotKeyHandlers = useMemo( () => ( { @@ -125,20 +123,20 @@ const Navigator = ( { updateFocus, register, focused }: NavigatorProps ) => { // If there's no Shabad to show, go back to the controller // if ( !lines.length ) return - const jumpLines = invert( getJumpLines( content ) ) - const nextLineId = getNextJumpLine( content ) + // const jumpLines = invert( getJumpLines( content ) ) + // const nextLineId = getNextJumpLine( { content, lineId } ) return ( e.preventDefault()}> - {lines.map( ( line ) => ( + {lines?.map( ( line ) => ( @@ -176,45 +174,46 @@ type BarProps = { export const Bar = ( { onHover }: BarProps ) => { const [ autoSelectHover, setAutoSelectHover ] = useState( false ) - const content = useContext( ContentContext ) - const { lineId, shabad, bani, mainLineId } = content - const { lines } = shabad || bani || {} - - if ( !lines ) return null + const { + content, + setNextContent, + setPreviousContent, + setPreviousLine, + setNextLine, + lineId, + line, + lines, + } = useContent() + const { mainLineId } = useTracker() - const currentLineIndex = findLineIndex( lines, lineId ) - const currentLine = lines[ currentLineIndex ] + if ( !line || !lines ) return null const resetHover = () => onHover( null ) const onUpClick = () => { - if ( !currentLine ) return + if ( !line ) return const firstLine = lines[ 0 ] // Go to the previous shabad if the first line is highlighted (but not for banis) if ( lineId === firstLine.id ) { - if ( bani ) return - - controller.previousShabad( shabad.orderId ) - } else controller.line( lines[ currentLineIndex - 1 ].id ) + setPreviousContent() + } else setPreviousLine() } const onDownClick = () => { - if ( !currentLine ) return + if ( !line ) return const lastLine = lines[ lines.length - 1 ] // Go to the previous shabad if the first line is highlighted (but not for banis) if ( lineId === lastLine.id ) { - if ( bani ) return - - controller.nextShabad( shabad.orderId ) - } else controller.line( lines[ currentLineIndex + 1 ].id ) + setNextContent() + } else setNextLine() } const onAutoToggle = () => { - if ( shabad ) controller.autoToggleShabad( content ) - else if ( bani ) controller.autoToggleBani( content ) + // if ( shabad ) controller.autoToggleShabad( content ) + // else if ( bani ) controller.autoToggleBani( content ) } const onAutoSelectHover = () => { @@ -237,7 +236,7 @@ export const Bar = ( { onHover }: BarProps ) => { const baniAutoSelectIcon = () => ( autoSelectHover ? faAngleDoubleRight : faExchangeAlt ) - const autoSelectIcon = shabad ? shabadAutoSelectIcon : baniAutoSelectIcon + const autoSelectIcon = content?.type === 'shabad' ? shabadAutoSelectIcon : baniAutoSelectIcon return (
@@ -250,7 +249,7 @@ export const Bar = ( { onHover }: BarProps ) => { onClick={onUpClick} /> - {currentLine && } + {line && } null, onHover = () => ( {} ) }: BottomBarProps ) => { const navigate = useNavigate() - const lines = useCurrentLines() + const { lines } = useContent() const resetHover = () => onHover( null ) @@ -135,7 +135,7 @@ const BottomBar = ( { renderContent = () => null, onHover = () => ( {} ) }: Bott
{renderContent()}
- {!!lines.length && ( + {lines?.length && ( null, onHover = () => ( {} ) }: Bott onHover( 'Clear' )} onMouseLeave={resetHover} /> @@ -157,8 +157,7 @@ const BottomBar = ( { renderContent = () => null, onHover = () => ( {} ) }: Bott } const Controller = ( props ) => { - const { shabad, bani } = useContext( ContentContext ) - const lines = useCurrentLines() + const { lines } = useContent() const previousLines = usePrevious( lines ) @@ -175,7 +174,7 @@ const Controller = ( props ) => { ] satisfies ToPathOption['to'][] // Redirect to navigator tab if on one of the redirectable pages - const isTransition = lines.length && lines !== previousLines + const isTransition = lines?.length && lines !== previousLines if ( isTransition && redirects.some( ( route ) => pathname.includes( route ) ) ) { void navigate( { to: '/presenter/controller/navigator' } ) diff --git a/apps/frontend/src/app/presenter/controller/search/-components/Result.tsx b/apps/frontend/src/app/presenter/controller/search/-components/Result.tsx index 00325c21..bbbdb2ff 100644 --- a/apps/frontend/src/app/presenter/controller/search/-components/Result.tsx +++ b/apps/frontend/src/app/presenter/controller/search/-components/Result.tsx @@ -5,6 +5,7 @@ import { forwardRef, useContext } from 'react' import { RecommendedSourcesContext, SettingsContext, WritersContext } from '~/helpers/contexts' import { LANGUAGE_NAMES, SOURCE_ABBREVIATIONS, TRANSLITERATORS } from '~/helpers/data' import { customiseLine, getTranslation } from '~/helpers/line' +import { setContent } from '~/services/content' import controller from '~/services/controller' type ResultProps = { @@ -20,7 +21,6 @@ type ResultProps = { translations: Record[], } - const Result = forwardRef( ( { gurmukhi, typeId, @@ -68,7 +68,7 @@ const Result = forwardRef( ( { const [ translitBeforeMatch, translitMatch, translitAfterMatch ] = highlight( transliteration ) // Send the shabad id and line id to the server on click - const onClick = () => controller.shabad( { shabadId, lineId } ) + const onClick = () => setContent( { id: shabadId, lineId, type: 'shabad' } ) // Helper render functions for citation const showCitation = showResultCitations && shabad?.section diff --git a/apps/frontend/src/app/presenter/controller/search/index.tsx b/apps/frontend/src/app/presenter/controller/search/index.tsx index c6c8fb1b..1d35747e 100644 --- a/apps/frontend/src/app/presenter/controller/search/index.tsx +++ b/apps/frontend/src/app/presenter/controller/search/index.tsx @@ -15,7 +15,7 @@ import { SEARCH_TYPES, } from '~/helpers/consts' import { SettingsContext } from '~/helpers/contexts' -import controller from '~/services/controller' +import { search, useSearchResults } from '~/services/search' import Result from './-components/Result' import getHighlighter from './-match-highlighter' @@ -62,12 +62,12 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => { const inputValue = useRef( initialInputValue ) const [ anchor, setAnchor ] = useState( initialAnchor ) - const [ results, setResults ] = useState( [] ) - const [ isInputFocused, setInputFocused ] = useState( false ) const inputRef = useRef( null ) + const { results, clearResults } = useSearchResults() + const onResults = useCallback( ( results ) => { setSearchedValue( inputValue.current ) setResults( results ) @@ -82,12 +82,16 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => { const doSearch = searchValue.length >= MIN_SEARCH_CHARS if ( doSearch ) { - controller.search( searchValue, searchType, { - translations: !!resultTranslationLanguage, - transliterations: !!resultTransliterationLanguage, - citations: !!showResultCitations, + search( { + query: searchValue, + type: searchType, + options: { + translations: !!resultTranslationLanguage, + transliterations: !!resultTransliterationLanguage, + citations: !!showResultCitations, + }, } ) - } else setResults( [] ) + } else clearResults() inputValue.current = searchValue setAnchor( anchor ) @@ -120,11 +124,6 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => { const highlightSearch = () => inputRef.current.select() - useEffect( () => { - controller.on( 'results', onResults ) - return () => { controller.off( 'results', onResults ) } - }, [ onResults ] ) - useEffect( () => { if ( inputValue.current ) onChange( { target: { value: `${anchor || ''}${inputValue.current}` } } ) }, [ @@ -174,7 +173,7 @@ const Search = ( { updateFocus, register, focused }: SearchProps ) => { /> - {results && results.map( ( result, index ) => ( + {results?.map( ( result, index ) => ( { onActive: () => setIsIdle( false ), } ) - const lines = useCurrentLines() + const { lines } = useContent() const { local: localSettings } = useContext( SettingsContext ) const { @@ -73,9 +72,10 @@ const Presenter = () => { const toggleController = () => void navigate( { to: isControllerOpen ? '/' : '/presenter/controller' } ) const { controllerZoom } = CLIENT_OPTIONS - const setZoom = ( controllerZoom: number ) => controller.setSettings( { - layout: { controllerZoom }, - } ) + // const setZoom = ( controllerZoom: number ) => controller.setSettings( { + // layout: { controllerZoom }, + // } ) + const setZoom = () => {} const zoomInController = () => setZoom( Math.min( controllerZoom.max, zoom + 0.1 ) ) const zoomOutController = () => setZoom( Math.max( controllerZoom.min, zoom - 0.1 ) ) @@ -112,7 +112,7 @@ const Presenter = () => { [ 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.clearDisplay.name ]: clearLine, [ GLOBAL_SHORTCUTS.toggleFullscreenController.name ]: toggleFullscreenController, [ GLOBAL_SHORTCUTS.toggleFullscreen.name ]: toggleFullscreen, [ GLOBAL_SHORTCUTS.quit.name ]: window.close, diff --git a/apps/frontend/src/app/screen-reader/index.lazy.tsx b/apps/frontend/src/app/screen-reader/index.lazy.tsx index 685065d2..7a969003 100644 --- a/apps/frontend/src/app/screen-reader/index.lazy.tsx +++ b/apps/frontend/src/app/screen-reader/index.lazy.tsx @@ -4,8 +4,8 @@ import { createLazyFileRoute } from '@tanstack/react-router' import classNames from 'classnames' import { useContext } from 'react' -import { ContentContext } from '~/helpers/contexts' import { classifyWords } from '~/helpers/line' +import { useContent } from '~/services/content' const titlesFuzzy = [ '<>', @@ -60,14 +60,12 @@ const pauriEndingRegex = /][\d]+]/ const isEndOfPauri = ( str: string ) => ( pauriEndingRegex.test( str ) || str.includes( 'bolo jI vwihgurU [' ) ) const ScreenReader = () => { - const { shabad, bani } = useContext( ContentContext ) - - const { lines = [] } = shabad ?? bani ?? {} + const { lines } = useContent() return (
- {lines.map( ( { gurmukhi, id } ) => ( + {lines?.map( ( { gurmukhi, id } ) => (

{ const { local: { hotkeys, display: { lineEnding } } } = useContext( SettingsContext ) - const lines = useCurrentLines() - const [ line ] = useCurrentLine() - const { typeId } = line || {} + const { lines, line } = useContent() // Get Shabad, writer, sources for getting the author - const { shabad } = useContext( ContentContext ) const writers = useContext( WritersContext ) const recommendedSources = useContext( RecommendedSourcesContext ) @@ -32,7 +30,7 @@ const CopyHotkeys = ( { children }: CopyHotkeysProps ) => { LANGUAGES.punjabi, LANGUAGES.spanish, ] ), - ( line ) => customiseLine( line, { lineEnding, typeId } ), + ( line ) => customiseLine( line, { lineEnding, typeId: line?.typeId } ), ) // Get all transliterators @@ -43,14 +41,14 @@ const CopyHotkeys = ( { children }: CopyHotkeysProps ) => { LANGUAGES.urdu, ] ), ( transliterate ) => () => transliterate( - customiseLine( line.gurmukhi, { lineEnding, typeId } ), + customiseLine( line?.gurmukhi, { lineEnding, typeId: line?.typeId } ), ), ) const getAuthor = () => { if ( !line ) return '' - const { sourceId, writerId } = shabad || line.shabad + const { sourceId, writerId } = content?.type === 'shabad' ? content.shabad : line.shabad const { sourcePage } = line const { pageNameEnglish: pageName } = recommendedSources[ sourceId ] @@ -59,7 +57,7 @@ const CopyHotkeys = ( { children }: CopyHotkeysProps ) => { return `${writerName} - ${SOURCE_ABBREVIATIONS[ sourceId ]} - ${pageName} ${sourcePage}` } - const getAllLines = () => lines.map( ( { gurmukhi } ) => gurmukhi ).join( ' ' ) + const getAllLines = () => lines?.map( ( { gurmukhi } ) => gurmukhi ).join( ' ' ) const copyToClipboard = useCopyToClipboard() diff --git a/apps/frontend/src/components/NavigatorHotkeys.tsx b/apps/frontend/src/components/NavigatorHotkeys.tsx index b9669494..4d8ebdd4 100644 --- a/apps/frontend/src/components/NavigatorHotkeys.tsx +++ b/apps/frontend/src/components/NavigatorHotkeys.tsx @@ -2,10 +2,10 @@ import { noop } from 'lodash' import { useCallback, useContext, useEffect } from 'react' import { getJumpLines } from '~/helpers/auto-jump' -import { ContentContext, HistoryContext, SettingsContext } from '~/helpers/contexts' +import { HistoryContext, SettingsContext } from '~/helpers/contexts' import { LINE_HOTKEYS, NAVIGATOR_SHORTCUTS } from '~/helpers/keyMap' -import { findLineIndex } from '~/helpers/line' -import { useCurrentLines, useWindowFocus } from '~/hooks' +import { useWindowFocus } from '~/hooks' +import { setLine, setNextContent, setPreviousContent, useContent } from '~/services/content' import controller from '~/services/controller' import GlobalHotKeys from './GlobalHotKeys' @@ -21,9 +21,8 @@ const NavigatorHotKeys = ( ) => { const { viewedLines } = useContext( HistoryContext ) - const content = useContext( ContentContext ) - const { lineId, mainLineId, nextLineId, shabad, bani } = content - const lines = useCurrentLines() + const { content, lineId } = useContent() + const lines = content?.lines const goFirstLine = () => { if ( !lines ) return @@ -31,8 +30,8 @@ const NavigatorHotKeys = ( const [ firstLine ] = lines // Go to the previous shabad if the first line is highlighted (but not for banis) - if ( !bani && lineId === firstLine.id ) controller.previousShabad( shabad.orderId ) - else controller.line( firstLine.id ) + if ( content.type === 'shabad' && lineId === firstLine.id ) setPreviousContent() + else setLine( firstLine.id ) } const goLastLine = () => { @@ -41,14 +40,14 @@ const NavigatorHotKeys = ( const lastLine = lines[ lines.length - 1 ] // Go to the next shabad if the last line is highlighted (but not for banis) - if ( !bani && lineId === lastLine.id ) controller.nextShabad( shabad.orderId ) - else controller.line( lastLine.id ) + if ( content.type === 'shabad' && lineId === lastLine.id ) setNextContent() + else setLine( lastLine.id ) } const autoToggle = useCallback( () => { - if ( shabad ) controller.autoToggleShabad( content ) - else if ( bani ) controller.autoToggleBani( content ) - }, [ shabad, bani, content ] ) + // if ( content.type === 'shabad' ) controller.autoToggleShabad( content ) + // else if ( content.type === 'bani' ) controller.autoToggleBani( content ) + }, [ content ] ) const restoreLine = () => { const ids = Object @@ -178,4 +177,4 @@ const NavigatorHotKeys = ( ) } -export default NavigatorHotKeys +export default ( { children } ) => children diff --git a/apps/frontend/src/components/ThemeLoader.tsx b/apps/frontend/src/components/ThemeLoader.tsx index 43b21947..102415d1 100644 --- a/apps/frontend/src/components/ThemeLoader.tsx +++ b/apps/frontend/src/components/ThemeLoader.tsx @@ -1,19 +1,18 @@ import defaultTheme from '@presenter/themes/presenter/Day.css?url' -import { useContext } from 'react' import { API_URL } from '~/helpers/consts' -import { StatusContext } from '~/helpers/contexts' +import { useStatus } from '~/services/status' const PRESENTER_THEMES_URL = `${API_URL}/themes/presenter` type ThemeLoaderProps = { name: string } const ThemeLoader = ( { name = 'Day' }: ThemeLoaderProps ) => { - const { connectedAt } = useContext( StatusContext ) + const { connectedAt } = useStatus() return ( diff --git a/apps/frontend/src/helpers/auto-jump.ts b/apps/frontend/src/helpers/auto-jump.ts index 337ef970..64c1d0b2 100644 --- a/apps/frontend/src/helpers/auto-jump.ts +++ b/apps/frontend/src/helpers/auto-jump.ts @@ -1,8 +1,8 @@ +import { ClientEventParameters } from '@presenter/contract' import { find, findIndex, findLastIndex, invert } from 'lodash' import memoize from 'memoizee' import { BANIS } from './data' -import { findLineIndex } from './line' const isBaniJumpLine = ( baniId, lines ) => ( { jumpLines }, @@ -26,13 +26,11 @@ const isBaniJumpLine = ( baniId, lines ) => ( return filter() } -export const getJumpLines = memoize( ( { shabad, bani } ) => { - if ( !( shabad || bani ) ) return {} - - const { lines } = shabad || bani +export const getJumpLines = memoize( ( content: ClientEventParameters['content:current'] ) => { + if ( !content?.lines ) return {} // Get a function for determining whether a line is jumpable - const isJumpLine = bani ? isBaniJumpLine( bani.id, lines ) : () => true + const isJumpLine = content.type === 'bani' ? isBaniJumpLine( content.id, content.lines ) : () => true // Go over each line, and tag which lines are jumpable const { jumpLines } = lines.reduce( ( { jumpLines, jumpIndex }, line, lineIndex ) => ( { @@ -51,17 +49,17 @@ export const getJumpLines = memoize( ( { shabad, bani } ) => { }, { primitive: true, max: 1, - normalizer: ( [ { bani, shabad } ] ) => JSON.stringify( { - shabadId: ( shabad ? shabad.id : null ), - baniId: ( bani ? bani.id : null ), + normalizer: ( [ content ] ) => JSON.stringify( { + shabadId: ( content?.type === 'shabad' ? content.shabad.id : null ), + baniId: ( content?.type === 'bani' ? content.bani.id : null ), } ), } ) -export const getBaniNextJumpLine = ( { bani, lineId } ) => { - const { lines } = bani +export const getBaniNextJumpLine = ( { content, lineId } ) => { + const { lines } = content // Get jump lines and current line index - const jumpLines = invert( getJumpLines( { bani } ) ) + const jumpLines = invert( getJumpLines( content ) ) const currentLineIndex = findLineIndex( lines, lineId ) const currentLine = lines[ currentLineIndex ] @@ -140,8 +138,8 @@ export const getBaniNextJumpLine = ( { bani, lineId } ) => { return baniNextLineId } -export const getNextJumpLine = ( { nextLineId, shabad, bani, lineId } ) => { - if ( !( shabad || bani ) ) return null +export const getNextJumpLine = ( { nextLineId, content, lineId } ) => { + if ( !content ) return null - return shabad ? nextLineId : getBaniNextJumpLine( { bani, lineId } ) + return content.type === 'shabad' ? nextLineId : getBaniNextJumpLine( { content, lineId } ) } diff --git a/apps/frontend/src/helpers/consts.ts b/apps/frontend/src/helpers/consts.ts index 2bb2697f..9d1dd5bc 100644 --- a/apps/frontend/src/helpers/consts.ts +++ b/apps/frontend/src/helpers/consts.ts @@ -16,7 +16,6 @@ export const HOST = window.location.hostname || 'localhost' export const PORT = window.location.port export const BASE_URL = `${HOST}:${PORT}` export const API_URL = `http://${BASE_URL}/api` -export const WS_URL = `ws://${BASE_URL}/api` /* Sentry Data Source Name */ export const SENTRY_DSN = 'https://51b714c1e7544cba86efb2cad85152ff@sentry.io/1363390' diff --git a/apps/frontend/src/helpers/contexts.tsx b/apps/frontend/src/helpers/contexts.tsx index 4d41b765..9e191486 100644 --- a/apps/frontend/src/helpers/contexts.tsx +++ b/apps/frontend/src/helpers/contexts.tsx @@ -1,4 +1,3 @@ -import { Bani, Line, Shabad } from '@presenter/contract' import { ComponentType, Context, createContext } from 'react' import { SettingsState } from './options' @@ -14,18 +13,6 @@ export const export const SettingsContext = createContext( {} as SettingsState ) -type Content = { - bani: Bani | null, - shabad: Shabad | null, - lineId: string, -} - -export const ContentContext = createContext( { - bani: null, - shabad: null, - lineId: '', -} ) - type TransitionHistory = { length: number, } @@ -43,15 +30,3 @@ type RecommendedSources = { export const RecommendedSourcesContext = createContext( {} as RecommendedSources ) export const WritersContext = createContext( {} ) - -export const BookmarksContext = createContext( [] ) - -type Status = { - connected: boolean, - connectedAt: Date | null, - status: string | null, -} - -export const StatusContext = createContext( { - connected: false, connectedAt: null, status: null, -} ) diff --git a/apps/frontend/src/helpers/line.ts b/apps/frontend/src/helpers/line.ts index ed6e6cf0..b8203b35 100644 --- a/apps/frontend/src/helpers/line.ts +++ b/apps/frontend/src/helpers/line.ts @@ -2,10 +2,9 @@ ** Currently shared with backend! Should be refactored. */ -import type { Line, RecommendedSources, Shabad, Translation } from '@presenter/contract' +import type { Content, Line, RecommendedSources, Shabad, Source, Translation } from '@presenter/contract' import { stripEndings, stripVishraams } from 'gurmukhi-utils' import vishraams from 'gurmukhi-utils/lib/vishraams.json' -import memoize from 'memoizee' import { LINE_TYPES, Translations, TRANSLITERATORS, Transliterators } from './data' @@ -16,17 +15,8 @@ export const sortBy = ( [ languageB ]: [string, any] ) => sortOrder[ languageA ] - sortOrder[ languageB ] -export const findLineIndex = memoize( - ( lines: Line[], lineId: string ) => lines.findIndex( ( { id } ) => id === lineId ), - { - primitive: true, - max: 5, - normalizer: ( [ , lineId ] ) => lineId, - }, -) - - type CustomiseLineParams = { lineEnding: boolean, typeId: number } - type LineTransformer = [boolean, ( text: string ) => string] +type CustomiseLineParams = { lineEnding: boolean, typeId: number } +type LineTransformer = [boolean, ( text: string ) => string] export const customiseLine = ( line: string, { lineEnding, typeId }: CustomiseLineParams ) => ( [ [ lineEnding, stripEndings ], @@ -64,7 +54,7 @@ export const partitionLine = ( line: string, strip = true ) => classifyWords( li }, [ [] ] ) type GetTranslationParams = { - shabad: Shabad, + content: Content, line: Line, sources: Source, recommendedSources: Source, @@ -72,9 +62,9 @@ export const partitionLine = ( line: string, strip = true ) => classifyWords( li } export const getTranslation = ( - { shabad, line, sources, recommendedSources, languageId }: GetTranslationParams + { content, line, sources, recommendedSources, languageId }: GetTranslationParams ) => { - const { sourceId } = shabad || line.shabad + const { sourceId } = content.shabad || line.shabad if ( !( sources?.[ sourceId ] ) ) return null @@ -94,7 +84,7 @@ export const getTranslation = ( type GetTranslationsParams = { languageIds: number[], line: Line, - shabad: Shabad | null, + content: Content, sources: RecommendedSources['sources'], recommendedSources: RecommendedSources['recommendedSources'], } diff --git a/apps/frontend/src/hooks/index.ts b/apps/frontend/src/hooks/index.ts index 43a5c7eb..e226a954 100644 --- a/apps/frontend/src/hooks/index.ts +++ b/apps/frontend/src/hooks/index.ts @@ -1,39 +1,19 @@ -import { Line } from '@presenter/contract' import copy from 'copy-to-clipboard' import { useSnackbar } from 'notistack' import { useContext, useEffect, useRef, useState } from 'react' import { isMac } from '~/helpers/consts' -import { ContentContext, RecommendedSourcesContext, SettingsContext } from '~/helpers/contexts' -import { findLineIndex, getTranslations } from '~/helpers/line' - -export const useCurrentLines = () => { - const { shabad, bani } = useContext( ContentContext ) - - const { lines = [] as Line[] } = shabad || bani || {} - - return lines -} - -export const useCurrentLine = () => { - const { lineId, shabad, bani } = useContext( ContentContext ) - const { lines = [] as Line[] } = shabad || bani || {} - - // Find the correct line in the Shabad - const lineIndex = findLineIndex( lines, lineId ) - const line = lineIndex > -1 ? lines[ lineIndex ] : null - - return [ line as Line, lineIndex ] as const -} +import { RecommendedSourcesContext, SettingsContext } from '~/helpers/contexts' +import { getTranslations } from '~/helpers/line' +import { useContent } from '~/services/content' export const useTranslations = ( languageIds: number[] ) => { - const { shabad } = useContext( ContentContext ) - const [ line ] = useCurrentLine() + const { content, line } = useContent() const recommendedSources = useContext( RecommendedSourcesContext ) const { local: { sources } = {} } = useContext( SettingsContext ) - return getTranslations( { shabad, line, recommendedSources, sources, languageIds } ) + return getTranslations( { content, line, recommendedSources, sources, languageIds } ) } export const useCopyToClipboard = () => { diff --git a/apps/frontend/src/index.tsx b/apps/frontend/src/index.tsx index b43887f6..98fe3ad3 100644 --- a/apps/frontend/src/index.tsx +++ b/apps/frontend/src/index.tsx @@ -5,9 +5,9 @@ import { createRoot } from 'react-dom/client' import Empty from './components/Empty' import { isDev } from './helpers/consts' import { routeTree } from './routeTree.gen' -import controller from './services/controller' +// import controller from './services/controller' -const settings = controller.readSettings() +// const settings = controller.readSettings() // if ( settings?.security?.displayAnalytics ) { // void analytics.initialise() diff --git a/apps/frontend/src/routeTree.gen.ts b/apps/frontend/src/routeTree.gen.ts index d9a076d2..0e05427c 100644 --- a/apps/frontend/src/routeTree.gen.ts +++ b/apps/frontend/src/routeTree.gen.ts @@ -286,30 +286,215 @@ declare module '@tanstack/react-router' { // Create and export the route tree -export const routeTree = rootRoute.addChildren({ - IndexRoute, - PresenterRouteRoute: PresenterRouteRoute.addChildren({ - PresenterControllerRouteRoute: PresenterControllerRouteRoute.addChildren({ - PresenterControllerIndexRoute, +interface PresenterControllerRouteRouteChildren { + PresenterControllerIndexRoute: typeof PresenterControllerIndexRoute + PresenterControllerBookmarksIndexRoute: typeof PresenterControllerBookmarksIndexRoute + PresenterControllerHistoryIndexRoute: typeof PresenterControllerHistoryIndexRoute + PresenterControllerNavigatorIndexRoute: typeof PresenterControllerNavigatorIndexRoute + PresenterControllerSearchIndexRoute: typeof PresenterControllerSearchIndexRoute +} + +const PresenterControllerRouteRouteChildren: PresenterControllerRouteRouteChildren = + { + PresenterControllerIndexRoute: PresenterControllerIndexRoute, + PresenterControllerBookmarksIndexRoute: PresenterControllerBookmarksIndexRoute, - PresenterControllerHistoryIndexRoute, + PresenterControllerHistoryIndexRoute: PresenterControllerHistoryIndexRoute, + PresenterControllerNavigatorIndexRoute: PresenterControllerNavigatorIndexRoute, - PresenterControllerSearchIndexRoute, - }), - }), - SettingsRouteRoute: SettingsRouteRoute.addChildren({ - SettingsIndexRoute, - SettingsClientCategoryRoute, - SettingsServerCategoryRoute, - SettingsClientHotkeysIndexRoute, - SettingsClientSourcesIndexRoute, - SettingsServerAboutIndexRoute, - SettingsToolsClosedCaptionsIndexRoute, - SettingsToolsOverlayIndexRoute, - }), - OverlayIndexLazyRoute, - ScreenReaderIndexLazyRoute, -}) + PresenterControllerSearchIndexRoute: PresenterControllerSearchIndexRoute, + } + +const PresenterControllerRouteRouteWithChildren = + PresenterControllerRouteRoute._addFileChildren( + PresenterControllerRouteRouteChildren, + ) + +interface PresenterRouteRouteChildren { + PresenterControllerRouteRoute: typeof PresenterControllerRouteRouteWithChildren +} + +const PresenterRouteRouteChildren: PresenterRouteRouteChildren = { + PresenterControllerRouteRoute: PresenterControllerRouteRouteWithChildren, +} + +const PresenterRouteRouteWithChildren = PresenterRouteRoute._addFileChildren( + PresenterRouteRouteChildren, +) + +interface SettingsRouteRouteChildren { + SettingsIndexRoute: typeof SettingsIndexRoute + SettingsClientCategoryRoute: typeof SettingsClientCategoryRoute + SettingsServerCategoryRoute: typeof SettingsServerCategoryRoute + SettingsClientHotkeysIndexRoute: typeof SettingsClientHotkeysIndexRoute + SettingsClientSourcesIndexRoute: typeof SettingsClientSourcesIndexRoute + SettingsServerAboutIndexRoute: typeof SettingsServerAboutIndexRoute + SettingsToolsClosedCaptionsIndexRoute: typeof SettingsToolsClosedCaptionsIndexRoute + SettingsToolsOverlayIndexRoute: typeof SettingsToolsOverlayIndexRoute +} + +const SettingsRouteRouteChildren: SettingsRouteRouteChildren = { + SettingsIndexRoute: SettingsIndexRoute, + SettingsClientCategoryRoute: SettingsClientCategoryRoute, + SettingsServerCategoryRoute: SettingsServerCategoryRoute, + SettingsClientHotkeysIndexRoute: SettingsClientHotkeysIndexRoute, + SettingsClientSourcesIndexRoute: SettingsClientSourcesIndexRoute, + SettingsServerAboutIndexRoute: SettingsServerAboutIndexRoute, + SettingsToolsClosedCaptionsIndexRoute: SettingsToolsClosedCaptionsIndexRoute, + SettingsToolsOverlayIndexRoute: SettingsToolsOverlayIndexRoute, +} + +const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren( + SettingsRouteRouteChildren, +) + +interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/presenter': typeof PresenterRouteRouteWithChildren + '/settings': typeof SettingsRouteRouteWithChildren + '/presenter/controller': typeof PresenterControllerRouteRouteWithChildren + '/settings/': typeof SettingsIndexRoute + '/overlay': typeof OverlayIndexLazyRoute + '/screen-reader': typeof ScreenReaderIndexLazyRoute + '/settings/client/$category': typeof SettingsClientCategoryRoute + '/settings/server/$category': typeof SettingsServerCategoryRoute + '/presenter/controller/': typeof PresenterControllerIndexRoute + '/presenter/controller/bookmarks': typeof PresenterControllerBookmarksIndexRoute + '/presenter/controller/history': typeof PresenterControllerHistoryIndexRoute + '/presenter/controller/navigator': typeof PresenterControllerNavigatorIndexRoute + '/presenter/controller/search': typeof PresenterControllerSearchIndexRoute + '/settings/client/hotkeys': typeof SettingsClientHotkeysIndexRoute + '/settings/client/sources': typeof SettingsClientSourcesIndexRoute + '/settings/server/about': typeof SettingsServerAboutIndexRoute + '/settings/tools/closedCaptions': typeof SettingsToolsClosedCaptionsIndexRoute + '/settings/tools/overlay': typeof SettingsToolsOverlayIndexRoute +} + +interface FileRoutesByTo { + '/': typeof IndexRoute + '/presenter': typeof PresenterRouteRouteWithChildren + '/settings': typeof SettingsIndexRoute + '/overlay': typeof OverlayIndexLazyRoute + '/screen-reader': typeof ScreenReaderIndexLazyRoute + '/settings/client/$category': typeof SettingsClientCategoryRoute + '/settings/server/$category': typeof SettingsServerCategoryRoute + '/presenter/controller': typeof PresenterControllerIndexRoute + '/presenter/controller/bookmarks': typeof PresenterControllerBookmarksIndexRoute + '/presenter/controller/history': typeof PresenterControllerHistoryIndexRoute + '/presenter/controller/navigator': typeof PresenterControllerNavigatorIndexRoute + '/presenter/controller/search': typeof PresenterControllerSearchIndexRoute + '/settings/client/hotkeys': typeof SettingsClientHotkeysIndexRoute + '/settings/client/sources': typeof SettingsClientSourcesIndexRoute + '/settings/server/about': typeof SettingsServerAboutIndexRoute + '/settings/tools/closedCaptions': typeof SettingsToolsClosedCaptionsIndexRoute + '/settings/tools/overlay': typeof SettingsToolsOverlayIndexRoute +} + +interface FileRoutesById { + '/': typeof IndexRoute + '/presenter': typeof PresenterRouteRouteWithChildren + '/settings': typeof SettingsRouteRouteWithChildren + '/presenter/controller': typeof PresenterControllerRouteRouteWithChildren + '/settings/': typeof SettingsIndexRoute + '/overlay/': typeof OverlayIndexLazyRoute + '/screen-reader/': typeof ScreenReaderIndexLazyRoute + '/settings/client/$category': typeof SettingsClientCategoryRoute + '/settings/server/$category': typeof SettingsServerCategoryRoute + '/presenter/controller/': typeof PresenterControllerIndexRoute + '/presenter/controller/bookmarks/': typeof PresenterControllerBookmarksIndexRoute + '/presenter/controller/history/': typeof PresenterControllerHistoryIndexRoute + '/presenter/controller/navigator/': typeof PresenterControllerNavigatorIndexRoute + '/presenter/controller/search/': typeof PresenterControllerSearchIndexRoute + '/settings/client/hotkeys/': typeof SettingsClientHotkeysIndexRoute + '/settings/client/sources/': typeof SettingsClientSourcesIndexRoute + '/settings/server/about/': typeof SettingsServerAboutIndexRoute + '/settings/tools/closedCaptions/': typeof SettingsToolsClosedCaptionsIndexRoute + '/settings/tools/overlay/': typeof SettingsToolsOverlayIndexRoute +} + +interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/presenter' + | '/settings' + | '/presenter/controller' + | '/settings/' + | '/overlay' + | '/screen-reader' + | '/settings/client/$category' + | '/settings/server/$category' + | '/presenter/controller/' + | '/presenter/controller/bookmarks' + | '/presenter/controller/history' + | '/presenter/controller/navigator' + | '/presenter/controller/search' + | '/settings/client/hotkeys' + | '/settings/client/sources' + | '/settings/server/about' + | '/settings/tools/closedCaptions' + | '/settings/tools/overlay' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/presenter' + | '/settings' + | '/overlay' + | '/screen-reader' + | '/settings/client/$category' + | '/settings/server/$category' + | '/presenter/controller' + | '/presenter/controller/bookmarks' + | '/presenter/controller/history' + | '/presenter/controller/navigator' + | '/presenter/controller/search' + | '/settings/client/hotkeys' + | '/settings/client/sources' + | '/settings/server/about' + | '/settings/tools/closedCaptions' + | '/settings/tools/overlay' + id: + | '/' + | '/presenter' + | '/settings' + | '/presenter/controller' + | '/settings/' + | '/overlay/' + | '/screen-reader/' + | '/settings/client/$category' + | '/settings/server/$category' + | '/presenter/controller/' + | '/presenter/controller/bookmarks/' + | '/presenter/controller/history/' + | '/presenter/controller/navigator/' + | '/presenter/controller/search/' + | '/settings/client/hotkeys/' + | '/settings/client/sources/' + | '/settings/server/about/' + | '/settings/tools/closedCaptions/' + | '/settings/tools/overlay/' + fileRoutesById: FileRoutesById +} + +interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PresenterRouteRoute: typeof PresenterRouteRouteWithChildren + SettingsRouteRoute: typeof SettingsRouteRouteWithChildren + OverlayIndexLazyRoute: typeof OverlayIndexLazyRoute + ScreenReaderIndexLazyRoute: typeof ScreenReaderIndexLazyRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PresenterRouteRoute: PresenterRouteRouteWithChildren, + SettingsRouteRoute: SettingsRouteRouteWithChildren, + OverlayIndexLazyRoute: OverlayIndexLazyRoute, + ScreenReaderIndexLazyRoute: ScreenReaderIndexLazyRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() /* prettier-ignore-end */ diff --git a/apps/frontend/src/services/bookmarks.ts b/apps/frontend/src/services/bookmarks.ts new file mode 100644 index 00000000..cc94adbf --- /dev/null +++ b/apps/frontend/src/services/bookmarks.ts @@ -0,0 +1,5 @@ +import { useAtomValue } from 'jotai' + +import websocketClient from './websocket-client' + +export const useBookmarks = () => useAtomValue( websocketClient.getAtom( 'content:bani:list' ) ) diff --git a/apps/frontend/src/services/content.ts b/apps/frontend/src/services/content.ts new file mode 100644 index 00000000..d84132bf --- /dev/null +++ b/apps/frontend/src/services/content.ts @@ -0,0 +1,36 @@ +import { ServerEventParameters } from '@presenter/contract' +import { atom, useAtomValue } from 'jotai' + +import websocketClient from './websocket-client' + +const linesByIdAtom = atom( ( get ) => { + const content = get( websocketClient.getAtom( 'content:current' ) ) + const lines = content?.lines + + return lines?.reduce( + ( acc, line, lineIndex ) => ( { ...acc, [ line.id ]: { line, lineIndex } } ), + {} as Record, + ) +} ) + +const contentAtom = atom( ( get ) => { + const content = get( websocketClient.getAtom( 'content:current' ) ) + + const lines = content?.lines + const lineId = get( websocketClient.getAtom( 'content:line:current' ) ) + + const linesById = get( linesByIdAtom ) + const line = lineId ? linesById?.[ lineId ] : undefined + + return { content, lineId, lines, ...line } +} ) + +export const useContent = () => useAtomValue( contentAtom ) + +export const setNextContent = () => websocketClient.json( 'content:open-next', undefined ) +export const setPreviousContent = () => websocketClient.json( 'content:open-previous', undefined ) +export const setPreviousLine = () => websocketClient.json( 'content:line:set-previous', undefined ) +export const setNextLine = () => websocketClient.json( 'content:line:set-next', undefined ) +export const setLine = ( lineId: ServerEventParameters['content:line:set-current'] ) => websocketClient.json( 'content:line:set-current', lineId ) +export const setContent = ( content: ServerEventParameters['content:open'] ) => websocketClient.json( 'content:open', content ) +export const clearLine = () => websocketClient.json( 'content:line:clear', undefined ) diff --git a/apps/frontend/src/services/controller.ts b/apps/frontend/src/services/controller.ts index 60c49fec..ef6042ce 100644 --- a/apps/frontend/src/services/controller.ts +++ b/apps/frontend/src/services/controller.ts @@ -1,5 +1,4 @@ import EventEmitter from 'eventemitter3' -import ReconnectingWebSocket from 'reconnecting-websocket' import { getNextJumpLine } from '~/helpers/auto-jump' import { isDev, isElectron, WS_URL } from '~/helpers/consts' @@ -15,35 +14,23 @@ type ShabadOptions = { } class Controller extends EventEmitter { - #socket: ReconnectingWebSocket #settings: SettingsState constructor() { super() - // Setup WebSocket connection to server - this.#socket = new ReconnectingWebSocket( WS_URL, undefined, { - reconnectionDelayGrowFactor: 1, - minReconnectionDelay: 300 + Math.random() * 200, - connectionTimeout: 1000, - } ) - // Initialise settings this.saveLocalSettings() this.#settings = { local: this.readSettings( true ) } as any - this.#socket.addEventListener( 'close', this.onClose ) - this.#socket.addEventListener( 'message', this.onMessage ) - this.#socket.addEventListener( 'open', this.onOpen ) - this.on( 'ready', this.onReady ) } sendJSON = ( event: string, payload?: any ) => { - const sendJSON = () => this.#socket.send( JSON.stringify( { event, payload } ) ) + // const sendJSON = () => this.#socket.send( JSON.stringify( { event, payload } ) ) - if ( this.#socket.readyState === 1 ) sendJSON() - else this.once( 'connected', sendJSON ) + // if ( this.#socket.readyState === 1 ) sendJSON() + // else this.once( 'connected', sendJSON ) } onOpen = () => { @@ -75,7 +62,7 @@ class Controller extends EventEmitter { } /** - essage = ( { data }: { data: any } ) => { + message = ( { data }: { data: any } ) => { const { event, payload } = JSON.parse( data ) this.emit( event, payload ) } diff --git a/apps/frontend/src/services/jotai.ts b/apps/frontend/src/services/jotai.ts new file mode 100644 index 00000000..1ce2b50f --- /dev/null +++ b/apps/frontend/src/services/jotai.ts @@ -0,0 +1,3 @@ +import { createStore } from 'jotai' + +export const store = createStore() diff --git a/apps/frontend/src/services/notifications.ts b/apps/frontend/src/services/notifications.ts new file mode 100644 index 00000000..42c776f4 --- /dev/null +++ b/apps/frontend/src/services/notifications.ts @@ -0,0 +1,9 @@ +import { useAtomValue } from 'jotai' + +import websocketClient from './websocket-client' + +export const useNotifications = () => { + const notifications = useAtomValue( websocketClient.getAtom( 'status' ) ) + + return notifications +} diff --git a/apps/frontend/src/services/search.ts b/apps/frontend/src/services/search.ts new file mode 100644 index 00000000..3d7a87b3 --- /dev/null +++ b/apps/frontend/src/services/search.ts @@ -0,0 +1,15 @@ +import { ServerEventParameters } from '@presenter/contract' +import { useAtom } from 'jotai' + +import websocketClient from './websocket-client' + +export const search = ( params: ServerEventParameters['search:query'] ) => websocketClient.json( 'search:query', params ) + +export const useSearchResults = () => { + const [ results, setResults ] = useAtom( websocketClient.getAtom( 'search:results' ) ) + + return { + results, + clearResults: () => setResults( [] ), + } +} diff --git a/apps/frontend/src/services/status.ts b/apps/frontend/src/services/status.ts new file mode 100644 index 00000000..fe07f9c3 --- /dev/null +++ b/apps/frontend/src/services/status.ts @@ -0,0 +1,30 @@ +import { atom, useAtomValue } from 'jotai' + +import websocketClient from './websocket-client' + +const connectedAtom = atom( false ) + +connectedAtom.onMount = ( set ) => { + const unlistenConnected = websocketClient.listen( 'connected', () => { + set( true ) + } ) + + const unlistenDisconnected = websocketClient.listen( 'disconnected', () => { + set( false ) + } ) + + return () => { + unlistenConnected() + unlistenDisconnected() + } +} + +const statusAtom = atom( ( get ) => { + const connected = get( connectedAtom ) + + return connected + ? { connected: true, connectedAt: new Date() } as const + : { connected: false, connectedAt: undefined } as const +} ) + +export const useStatus = () => useAtomValue( statusAtom ) diff --git a/apps/frontend/src/services/tracker.ts b/apps/frontend/src/services/tracker.ts new file mode 100644 index 00000000..30ca74fb --- /dev/null +++ b/apps/frontend/src/services/tracker.ts @@ -0,0 +1,10 @@ +import { useAtomValue } from 'jotai' + +import websocketClient from './websocket-client' + +export const useTracker = () => { + const mainLineId = useAtomValue( websocketClient.getAtom( 'content:tracker:main-line' ) ) + const nextLineId = useAtomValue( websocketClient.getAtom( 'content:tracker:next-line' ) ) + + return { mainLineId, nextLineId } +} diff --git a/apps/frontend/src/services/websocket-client.ts b/apps/frontend/src/services/websocket-client.ts new file mode 100644 index 00000000..1a7650ab --- /dev/null +++ b/apps/frontend/src/services/websocket-client.ts @@ -0,0 +1,96 @@ +import { ClientEvent, ClientEventParameters, clientEvents, ServerEvent, ServerEventParameters } from '@presenter/contract' +import { decode, encode } from '@presenter/swiss-knife' +import EventEmitter from 'eventemitter3' +import { atom, WritableAtom } from 'jotai' +import { WebSocket } from 'partysocket' + +import { BASE_URL } from '~/helpers/consts' + +import { store } from './jotai' + +const WS_URL = `ws://${BASE_URL}/api` + +type ClientEvents = { + 'connected': [undefined], + 'disconnected': [undefined], +} & { + [Event in ClientEvent]: [ClientEventParameters[Event]] +} + +const createWebSocketClient = () => { + const emitter = new EventEmitter() + + const socket = new WebSocket( WS_URL, undefined, { + minReconnectionDelay: 300 + Math.random() * 200, + connectionTimeout: 1000, + } ) + + socket.addEventListener( 'open', () => emitter.emit( 'connected', undefined ) ) + socket.addEventListener( 'close', () => emitter.emit( 'disconnected', undefined ) ) + socket.addEventListener( 'message', ( { data } ) => { + const { event, payload } = decode<{ + event: ClientEvent, + payload: ClientEventParameters[ClientEvent], + }>( data as string ) + + emitter.emit( event, payload ) + } ) + + const send = ( data: string ) => { + if ( socket.readyState === WebSocket.OPEN ) socket.send( data ) + else emitter.once( 'connected', () => socket.send( data ) ) + } + + const json = ( + event: Event, + payload: ServerEventParameters[Event] + ) => send( encode( { event, payload } ) ) + + const listen = ( + event: Event, + listener: ( payload: ClientEvents[Event][0] ) => void + ) => { + emitter.on( event, listener ) + return () => emitter.off( event, listener ) + } + + const atoms = new Map< + keyof ClientEvents, + WritableAtom + >() + + const getAtom = ( event: Event ) => { + if ( atoms.has( event ) ) { + return atoms.get( event ) as WritableAtom< + ClientEvents[Event][0] | undefined, + [ClientEvents[Event][0] | undefined], + void + > + } + + const a = atom( null ) + atoms.set( event, a ) + + return a + } + + // Listen to events and set atoms at this point so that we don't miss any events + clientEvents.forEach( + /* eslint-disable-next-line + @typescript-eslint/no-unsafe-argument, + @typescript-eslint/no-explicit-any + */ + ( event ) => emitter.on( event, ( payload ) => store.set( getAtom( event ) as any, payload ) ) + ) + + return { + json, + on: emitter.on.bind( emitter ), + once: emitter.once.bind( emitter ), + off: emitter.off.bind( emitter ), + listen, + getAtom, + } +} + +export default createWebSocketClient() diff --git a/package-lock.json b/package-lock.json index 41fd4c98..420e0587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,8 +123,251 @@ "electron-notarize": "^1.2.1" }, "devDependencies": { - "electron": "18.2.0", - "npm-run-all": "^4.1.5" + "electron": "^32" + } + }, + "apps/electron/node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "apps/electron/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "apps/electron/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "apps/electron/node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "apps/electron/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/electron/node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "apps/electron/node_modules/electron": { + "version": "32.2.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-32.2.2.tgz", + "integrity": "sha512-c7TRE42JcgEmJ4elJyCdKk/2os0UX7YMkRDeXBkxFEoM34iX1/2x+c5T9PgeroKz8FEG7omRU5TvjulqVtXvdw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "apps/electron/node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "apps/electron/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/electron/node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "apps/electron/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "apps/electron/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/electron/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/electron/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "apps/electron/node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "apps/electron/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "apps/frontend": { @@ -140,6 +383,7 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@mui/material": "^5.16.6", + "@presenter/swiss-knife": "*", "@presenter/themes": "*", "@sentry/browser": "^6.19.7", "@tanstack/react-router": "^1.49.2", @@ -150,15 +394,16 @@ "eventemitter3": "^5.0.1", "gurmukhi-utils": "^3.2.2", "is-mobile": "^3.1.1", + "jotai": "^2.10.1", "lodash": "^4.17.21", "memoizee": "^0.4.17", "notistack": "^3.0.1", + "partysocket": "^1.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hotkeys": "^2.0.0", "react-idle-timer": "^5.7.2", "react-transition-group": "^4.4.5", - "reconnecting-websocket": "^4.4.0", "scroll-into-view": "^1.16.0", "zod": "^3.8.2-alpha.6" }, @@ -2776,6 +3021,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/compression": { "version": "1.7.5", "dev": true, @@ -2849,6 +3107,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "dev": true, @@ -2900,6 +3165,16 @@ "license": "MIT", "peer": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.7", "dev": true, @@ -3032,6 +3307,16 @@ "@types/react": "*" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/scroll-into-view": { "version": "1.16.4", "dev": true, @@ -3110,6 +3395,17 @@ "version": "21.0.3", "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "dev": true, @@ -3958,6 +4254,7 @@ "version": "1.0.3", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.5", @@ -4592,6 +4889,16 @@ "node": ">=0.10.0" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, "node_modules/cacheable-request": { "version": "6.1.0", "license": "MIT", @@ -5338,6 +5645,7 @@ "version": "1.0.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", @@ -5354,6 +5662,7 @@ "version": "1.0.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -5370,6 +5679,7 @@ "version": "1.0.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", @@ -6222,6 +6532,7 @@ "version": "1.23.3", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", @@ -6342,6 +6653,7 @@ "version": "1.0.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -6353,6 +6665,7 @@ "version": "2.0.3", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "get-intrinsic": "^1.2.4", "has-tostringtag": "^1.0.2", @@ -6375,6 +6688,7 @@ "version": "1.2.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -7983,6 +8297,7 @@ "version": "1.1.6", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8082,6 +8397,7 @@ "version": "1.0.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.5", "es-errors": "^1.3.0", @@ -8677,6 +8993,20 @@ "node": ">= 6" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "license": "MIT", @@ -9156,6 +9486,7 @@ "version": "1.0.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-typed-array": "^1.1.13" }, @@ -9316,6 +9647,7 @@ "version": "2.0.3", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -9488,6 +9820,7 @@ "version": "1.1.13", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "which-typed-array": "^1.1.14" }, @@ -9527,6 +9860,7 @@ "version": "1.0.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -9766,6 +10100,27 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jotai": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.10.1.tgz", + "integrity": "sha512-4FycO+BOTl2auLyF2Chvi6KTDqdsdDDtpaL/WHQMs8f3KS1E3loiUShQzAzFA/sMU5cJ0hz/RT1xum9YbG/zaA==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/joycon": { "version": "3.1.1", "dev": true, @@ -9880,8 +10235,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", @@ -9972,7 +10326,6 @@ "version": "4.5.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -10094,39 +10447,13 @@ "version": "1.2.4", "license": "MIT" }, - "node_modules/load-json-file": { - "version": "4.0.0", + "node_modules/local-pkg": { + "version": "0.5.0", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/local-pkg": { - "version": "0.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" }, "engines": { "node": ">=14" @@ -10440,13 +10767,6 @@ "node": ">=0.12" } }, - "node_modules/memorystream": { - "version": "0.3.1", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/merge-descriptors": { "version": "1.0.1", "license": "MIT" @@ -11202,175 +11522,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm-run-all/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/npm-run-all/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/npm-run-all/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/npm-run-all/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/npm-run-all/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/npm-run-all/node_modules/path-key": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/npm-run-all/node_modules/shebang-command": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-all/node_modules/shebang-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-all/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/npm-run-path": { "version": "5.3.0", "dev": true, @@ -12001,6 +12152,27 @@ "node": ">= 0.8" } }, + "node_modules/partysocket": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.0.2.tgz", + "integrity": "sha512-rAFOUKImaq+VBk2B+2RTBsWEvlnarEP53nchoUHzpVs8V6fG2/estihOTslTQUWHVuHEKDL5k8htG8K3TngyFA==", + "license": "ISC", + "dependencies": { + "event-target-shim": "^6.0.2" + } + }, + "node_modules/partysocket/node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/pascalcase": { "version": "0.1.1", "license": "MIT", @@ -12114,21 +12286,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.3.1", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pify": { "version": "3.0.0", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=4" } @@ -12593,6 +12754,19 @@ "version": "4.0.4", "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "license": "MIT", @@ -12722,54 +12896,6 @@ "node": ">=6" } }, - "node_modules/read-pkg": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "dev": true, - "license": "ISC" - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "license": "MIT", @@ -12818,10 +12944,6 @@ "node": ">= 0.10" } }, - "node_modules/reconnecting-websocket": { - "version": "4.4.0", - "license": "MIT" - }, "node_modules/redent": { "version": "3.0.0", "dev": true, @@ -13205,6 +13327,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-dir": { "version": "1.0.1", "license": "MIT", @@ -13424,6 +13553,7 @@ "version": "1.1.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "get-intrinsic": "^1.2.4", @@ -13466,6 +13596,7 @@ "version": "1.0.3", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", @@ -13734,14 +13865,6 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/shelljs": { "version": "0.8.3", "license": "BSD-3-Clause", @@ -14702,23 +14825,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.padend": { - "version": "3.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.repeat": { "version": "1.0.0", "dev": true, @@ -14733,6 +14839,7 @@ "version": "1.2.9", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -14750,6 +14857,7 @@ "version": "1.0.8", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -14763,6 +14871,7 @@ "version": "1.0.8", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -14800,6 +14909,7 @@ "version": "3.0.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -15479,6 +15589,7 @@ "version": "1.0.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -15492,6 +15603,7 @@ "version": "1.0.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -15510,6 +15622,7 @@ "version": "1.0.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", @@ -15529,6 +15642,7 @@ "version": "1.0.6", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -15583,6 +15697,7 @@ "version": "1.0.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2",