Skip to content

Commit

Permalink
feat: event finder
Browse files Browse the repository at this point in the history
  • Loading branch information
cpvalente committed Oct 20, 2024
1 parent cf40001 commit 7a4efcf
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 74 deletions.
2 changes: 1 addition & 1 deletion apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/open-sans": "^5.0.28",
"@mantine/hooks": "^7.6.2",
"@mantine/hooks": "^7.13.3",
"@react-icons/all-files": "^4.1.0",
"@sentry/react": "^8.19.0",
"@tanstack/react-query": "^5.17.9",
Expand Down
60 changes: 19 additions & 41 deletions apps/client/src/features/editors/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { lazy, useCallback, useEffect } from 'react';
import { IconButton, useDisclosure } from '@chakra-ui/react';
import { useHotkeys } from '@mantine/hooks';
import { IoApps } from '@react-icons/all-files/io5/IoApps';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';

import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
import useElectronEvent from '../../common/hooks/useElectronEvent';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import AppSettings from '../app-settings/AppSettings';
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
import { EditorOverview } from '../overview/Overview';

import Finder from './finder/Finder';

import styles from './Editor.module.scss';

const Rundown = lazy(() => import('../rundown/RundownExport'));
Expand All @@ -19,47 +21,8 @@ const MessageControl = lazy(() => import('../control/message/MessageControlExpor

export default function Editor() {
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
const { isElectron } = useElectronEvent();
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();

const toggleSettings = useCallback(() => {
if (isSettingsOpen) {
close();
} else {
setLocation('project');
}
}, [close, isSettingsOpen, setLocation]);

// Handle keyboard shortcuts
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
// handle held key
if (event.repeat) return;

// check if the ctrl key is pressed
if (event.ctrlKey || event.metaKey) {
// ctrl + , (settings)
if (event.key === ',') {
toggleSettings();
event.preventDefault();
event.stopPropagation();
}
}
},
[toggleSettings],
);

// register ctrl + , to open settings
useEffect(() => {
if (isElectron) {
document.addEventListener('keydown', handleKeyPress);
}
return () => {
if (isElectron) {
document.removeEventListener('keydown', handleKeyPress);
}
};
}, [handleKeyPress, isElectron]);
const { isOpen: isFinderOpen, onToggle: onFinderToggle, onClose: onFinderClose } = useDisclosure();

useWindowTitle('Editor');

Expand All @@ -72,8 +35,23 @@ export default function Editor() {
}
}, [setLocation]);

const toggleSettings = useCallback(() => {
if (isSettingsOpen) {
close();
} else {
setLocation('project');
}
}, [close, isSettingsOpen, setLocation]);

useHotkeys([
['mod + ,', toggleSettings],
['mod + f', onFinderToggle],
['Escape', onFinderClose],
]);

return (
<div className={styles.mainContainer} data-testid='event-editor'>
<Finder isOpen={isFinderOpen} onClose={onFinderClose} />
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
<EditorOverview>
<IconButton
Expand Down
64 changes: 64 additions & 0 deletions apps/client/src/features/editors/finder/Finder.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.entry,
.empty,
.error {
padding-inline: 0.5rem;
font-size: 1rem;
height: 3rem;

display: flex;
align-items: center;
justify-content: space-between;
}

.entry[data-selected='true'] {
background-color: $blue-700;
}

.empty {
color: $label-gray;
}

.error {
color: $error-red;
}

.data {
display: grid;
grid-template-areas:
'index cue'
'index title';
column-gap: 1rem;
grid-template-rows: min-content 1fr;

.index {
grid-area: index;
background-color: $gray-1000;
border-radius: 2px;
padding-block: 0.25rem;
width: 3.5rem;
align-self: center;
text-align: center;
}

.title {
grid-area: title;
}

.cue {
grid-area: cue;
font-size: calc(1rem - 2px);
color: $label-gray;
max-height: 1em;
min-height: 0;
}
}

.footer {
font-size: calc(1rem - 2px);
color: $label-gray;
}

.em {
color: $ui-white;
margin-inline: 0.25rem;
}
76 changes: 76 additions & 0 deletions apps/client/src/features/editors/finder/Finder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { KeyboardEvent, useState } from 'react';
import { Input, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay } from '@chakra-ui/react';
import { useDebouncedCallback } from '@mantine/hooks';
import { isOntimeEvent, SupportedEvent } from 'ontime-types';

import { useEventSelection } from '../../rundown/useEventSelection';

import useFinder from './useFinder';

import style from './Finder.module.scss';

interface FinderProps {
isOpen: boolean;
onClose: () => void;
}

export default function Finder(props: FinderProps) {
const { isOpen, onClose } = props;
const { find, results, error } = useFinder();
const [selected, setSelected] = useState<number>(0);

const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
const debouncedFind = useDebouncedCallback(find, 100);

const navigate = (event: KeyboardEvent<HTMLDivElement>) => {
// all operations need results
if (results.length === 0) {
return;
}
if (event.key === 'ArrowDown') {
setSelected((prev) => (prev + 1) % results.length);
}
if (event.key === 'ArrowUp') {
setSelected((prev) => (prev - 1 + results.length) % results.length);
}
if (event.key === 'Enter') {
const selectedEvent = results[selected];
setSelectedEvents({ id: selectedEvent.id, index: selectedEvent.index, selectMode: 'click' });
onClose();
}
};

return (
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
<ModalOverlay />
<ModalContent maxWidth='40vw'>
<ModalBody onKeyDown={navigate}>
<Input size='lg' onChange={debouncedFind} variant='ontime-filled' placeholder='Search...' />
<ul>
{error && <li className={style.error}>{error}</li>}
{results.length === 0 && <li className={style.empty}>No results</li>}
{results.length > 0 &&
results.map((event, index) => {
const isSelected = selected === index;
const displayIndex = event.type === SupportedEvent.Block ? '-' : event.index;
return (
<li key={event.id} className={style.entry} data-selected={isSelected}>
<div className={style.data}>
<div className={style.index}>{displayIndex}</div>
{isOntimeEvent(event) && <div className={style.cue}>{event.cue}</div>}
<div className={style.title}>{event.title}</div>
</div>
{isSelected && <span>Go ⏎</span>}
</li>
);
})}
</ul>
</ModalBody>
<ModalFooter className={style.footer}>
Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or
<span className={style.em}>title</span> to filter search
</ModalFooter>
</ModalContent>
</Modal>
);
}
Loading

0 comments on commit 7a4efcf

Please sign in to comment.