Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
"view": "View",
"entryId": "Entry ID",
"login": "Login",
"clear": "clear"
"clear": "clear",
"complete": "complete",
"video": "video",
"key": "key",
"primary": "primay"
},
"languages": {
"en": "English",
Expand All @@ -40,7 +44,7 @@
"newStudy": "New Study",
"studyControl": "Study Control",
"entryControl": "Entry Control",
"downloadTags": "Download Tags",
"viewTags": "Download Tags",
"datasets": "Datasets",
"datasetControl": "Dataset Control",
"projectAccess": "Project Access",
Expand Down Expand Up @@ -117,6 +121,9 @@
"login": {
"selectOrg": "Select an Organization to Login",
"redirectToOrg": "Redirect to Organization Login"
},
"tagView": {
"originalEntry": "Original Entry"
}
}
}
4 changes: 2 additions & 2 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { StudyControl } from './pages/studies/StudyControl';
import { ProjectAccess } from './pages/datasets/ProjectAccess';
import { ProjectUserPermissions } from './pages/projects/ProjectUserPermissions';
import { StudyUserPermissions } from './pages/studies/UserPermissions';
import { DownloadTags } from './pages/studies/DownloadTags';
import { TagView } from './pages/studies/TagView';
import { DatasetControls } from './pages/datasets/DatasetControls';
import { AuthProvider, useAuth, AUTH_TOKEN_STR } from './context/Auth.context';
import { AdminGuard } from './guards/AdminGuard';
Expand Down Expand Up @@ -125,7 +125,7 @@ const MyRoutes: FC = () => {
<Route path={'/study/controls'} element={<StudyControl />} />
<Route path={'/study/permissions'} element={<StudyUserPermissions />} />
<Route path={'/study/entries'} element={<EntryControls />} />
<Route path={'/study/tags'} element={<DownloadTags />} />
<Route path={'/study/tags'} element={<TagView />} />
<Route path={'/successpage'} element={<SuccessPage />} />
<Route path={'/dataset/controls'} element={<DatasetControls />} />
<Route path={'/dataset/projectaccess'} element={<ProjectAccess />} />
Expand Down
96 changes: 3 additions & 93 deletions packages/client/src/components/EntryView.component.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { Box } from '@mui/material';
import { Entry } from '../graphql/graphql';
import { useEffect, useRef } from 'react';
import { VideoViewProps, VideoEntryView } from './VideoView.component';

export interface EntryViewProps {
export interface EntryViewProps extends Omit<VideoViewProps, 'url'> {
entry: Entry;
width: number;
pauseFrame?: 'start' | 'end' | 'middle';
autoPlay?: boolean;
mouseOverControls?: boolean;
displayControls?: boolean;
}

export const EntryView: React.FC<EntryViewProps> = (props) => {
Expand All @@ -17,7 +12,7 @@ export const EntryView: React.FC<EntryViewProps> = (props) => {

const getEntryView = (props: EntryViewProps) => {
if (props.entry.contentType.startsWith('video/')) {
return <VideoEntryView {...props} />;
return <VideoEntryView {...props} url={props.entry.signedUrl} />;
}
if (props.entry.contentType.startsWith('image/')) {
return <ImageEntryView {...props} />;
Expand All @@ -26,91 +21,6 @@ const getEntryView = (props: EntryViewProps) => {
return <p>Placeholder</p>;
};

// TODO: Add in ability to control video play, pause, and middle frame selection
const VideoEntryView: React.FC<EntryViewProps> = (props) => {
const videoRef = useRef<HTMLVideoElement>(null);

/** Start the video at the begining */
const handleStart: React.MouseEventHandler = () => {
if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) {
return;
}
videoRef.current.currentTime = 0;
videoRef.current?.play();
};

/** Stop the video */
const handleStop: React.MouseEventHandler = () => {
if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) {
return;
}
videoRef.current.pause();
setPauseFrame();
};

/** Set the video to the middle frame */
const setPauseFrame = async () => {
if (!videoRef.current) {
return;
}

if (!props.pauseFrame || props.pauseFrame === 'middle') {
const duration = await getDuration();
videoRef.current.currentTime = duration / 2;
} else if (props.pauseFrame === 'start') {
videoRef.current.currentTime = 0;
}
};

/** Get the duration, there is a known issue on Chrome with some audio/video durations */
const getDuration = async () => {
if (!videoRef.current) {
return 0;
}

const video = videoRef.current!;

// If the duration is infinity, this is part of a Chrome bug that causes
// some durations to not load for audio and video. The StackOverflow
// link below discusses the issues and possible solutions
// Then, wait for the update event to be triggered
await new Promise<void>((resolve, _reject) => {
video.ontimeupdate = () => {
// Remove the callback
video.ontimeupdate = () => {};
// Reset the time
video.currentTime = 0;
resolve();
};

video.currentTime = 1e101;
});

// Now try to get the duration again
return video.duration;
};

// Set the video to the middle frame when the video is loaded
useEffect(() => {
setPauseFrame();
}, [videoRef.current]);

return (
<Box sx={{ maxWidth: props.width }}>
<video
width={props.width}
onMouseEnter={handleStart}
onMouseLeave={handleStop}
ref={videoRef}
autoPlay={props.autoPlay}
controls={props.displayControls}
>
<source src={props.entry.signedUrl} />
</video>
</Box>
);
};

const ImageEntryView: React.FC<EntryViewProps> = (props) => {
return (
<Box sx={{ maxWidth: props.width }}>
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/components/SideBar.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const SideBar: FC<SideBarProps> = ({ open, drawerWidth }) => {
visible: (p) => p!.studyAdmin
},
{ name: t('menu.entryControl'), action: () => navigate('/study/entries'), visible: (p) => p!.studyAdmin },
{ name: t('menu.downloadTags'), action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin }
{ name: t('menu.viewTags'), action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin }
]
},
{
Expand Down
95 changes: 95 additions & 0 deletions packages/client/src/components/VideoView.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useRef, useEffect } from 'react';
import { Box } from '@mui/material';

export interface VideoViewProps {
url: string;
width: number;
pauseFrame?: 'start' | 'end' | 'middle';
autoPlay?: boolean;
mouseOverControls?: boolean;
displayControls?: boolean;
}

export const VideoEntryView: React.FC<VideoViewProps> = (props) => {
const videoRef = useRef<HTMLVideoElement>(null);

/** Start the video at the begining */
const handleStart: React.MouseEventHandler = () => {
if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) {
return;
}
videoRef.current.currentTime = 0;
videoRef.current?.play();
};

/** Stop the video */
const handleStop: React.MouseEventHandler = () => {
if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) {
return;
}
videoRef.current.pause();
setPauseFrame();
};

/** Set the video to the middle frame */
const setPauseFrame = async () => {
if (!videoRef.current) {
return;
}

if (!props.pauseFrame || props.pauseFrame === 'middle') {
const duration = await getDuration();
videoRef.current.currentTime = duration / 2;
} else if (props.pauseFrame === 'start') {
videoRef.current.currentTime = 0;
}
};

/** Get the duration, there is a known issue on Chrome with some audio/video durations */
const getDuration = async () => {
if (!videoRef.current) {
return 0;
}

const video = videoRef.current!;

// If the duration is infinity, this is part of a Chrome bug that causes
// some durations to not load for audio and video. The StackOverflow
// link below discusses the issues and possible solutions
// Then, wait for the update event to be triggered
await new Promise<void>((resolve, _reject) => {
video.ontimeupdate = () => {
// Remove the callback
video.ontimeupdate = () => {};
// Reset the time
video.currentTime = 0;
resolve();
};

video.currentTime = 1e101;
});

// Now try to get the duration again
return video.duration;
};

// Set the video to the middle frame when the video is loaded
useEffect(() => {
setPauseFrame();
}, [videoRef.current]);

return (
<Box sx={{ maxWidth: props.width }}>
<video
width={props.width}
onMouseEnter={handleStart}
onMouseLeave={handleStop}
ref={videoRef}
autoPlay={props.autoPlay}
controls={props.displayControls}
>
<source src={props.url} />
</video>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView';
import { useLexiconByKeyQuery } from '../../../graphql/lex';
import { useEffect, useState } from 'react';
import { VideoEntryView } from '../../VideoView.component';
import i18next from 'i18next';

const AslLexGridViewVideo: React.FC<TagColumnViewProps> = ({ data }) => {
const [videoUrl, setVideoUrl] = useState<string | null>(null);

const lexiconByKeyResult = useLexiconByKeyQuery({
variables: { lexicon: import.meta.env.VITE_ASL_LEXICON_ID, key: data }
});

useEffect(() => {
if (lexiconByKeyResult.data) {
setVideoUrl(lexiconByKeyResult.data.lexiconByKey.video);
}
}, [lexiconByKeyResult]);

return (
<>
{videoUrl && (
<VideoEntryView
url={videoUrl}
width={300}
pauseFrame="middle"
autoPlay={true}
mouseOverControls={true}
displayControls={true}
/>
)}
</>
);
};

const AslLexGridViewKey: React.FC<TagColumnViewProps> = ({ data }) => {
return data;
};

const AslLexGridViewPrimary: React.FC<TagColumnViewProps> = ({ data }) => {
const [primary, setPrimary] = useState<string | null>(null);

const lexiconByKeyResult = useLexiconByKeyQuery({
variables: { lexicon: import.meta.env.VITE_ASL_LEXICON_ID, key: data }
});

useEffect(() => {
if (lexiconByKeyResult.data) {
setPrimary(lexiconByKeyResult.data.lexiconByKey.primary);
}
}, [lexiconByKeyResult]);

return primary || '';
};

export const aslLexTest: TagViewTest = (uischema, _schema, _context) => {
if (
uischema.options != undefined &&
uischema.options.customType != undefined &&
uischema.options.customType == 'asl-lex'
) {
return 5;
}
return NOT_APPLICABLE;
};

export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => {
return [
{
field: `${property}-video`,
headerName: `${property}: ${i18next.t('common.video')}`,
width: 300,
renderCell: (params) =>
params.row.data &&
params.row.data[property] && (
<AslLexGridViewVideo data={params.row.data[property]} schema={schema} uischema={uischema} />
)
},
{
field: `${property}-key`,
headerName: `${property}: ${i18next.t('common.key')}`,
renderCell: (params) =>
params.row.data &&
params.row.data[property] && (
<AslLexGridViewKey data={params.row.data[property]} schema={schema} uischema={uischema} />
)
},
{
field: `${property}-primary`,
headerName: `${property}: ${i18next.t('common.primary')}`,
renderCell: (params) =>
params.row.data &&
params.row.data[property] && (
<AslLexGridViewPrimary data={params.row.data[property]} schema={schema} uischema={uischema} />
)
}
];
};
Loading