Skip to content

Commit

Permalink
Video element selector on app landing page
Browse files Browse the repository at this point in the history
  • Loading branch information
killergerbah committed Oct 20, 2024
1 parent 3801074 commit 8251d03
Show file tree
Hide file tree
Showing 21 changed files with 442 additions and 203 deletions.
61 changes: 54 additions & 7 deletions common/app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DownloadAudioMessage,
CardTextFieldValues,
ImageErrorCode,
RequestSubtitlesResponse,
} from '@project/common';
import { createTheme } from '@project/common/theme';
import { AsbplayerSettings, Profile } from '@project/common/settings';
Expand Down Expand Up @@ -53,6 +54,7 @@ import { useAppKeyBinder } from '../hooks/use-app-key-binder';
import { useAnki } from '../hooks/use-anki';
import { usePlaybackPreferences } from '../hooks/use-playback-preferences';
import { MiningContext } from '../services/mining-context';
import { timeDurationDisplay } from '../services/util';

const latestExtensionVersion = '1.5.0';
const extensionUrl =
Expand Down Expand Up @@ -679,7 +681,16 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,
for (let i = 0; i < availableTabs.length; ++i) {
const t1 = availableTabs[i];
const t2 = tabs[i];
if (t1.id !== t2.id || t1.title !== t2.title || t1.src !== t2.src) {
if (
t1.id !== t2.id ||
t1.title !== t2.title ||
t1.src !== t2.src ||
t1.faviconUrl !== t2.faviconUrl ||
t1.subscribed !== t2.subscribed ||
t1.synced !== t2.synced ||
t1.syncedTimestamp !== t2.syncedTimestamp ||
t1.faviconUrl !== t2.faviconUrl
) {
update = true;
break;
}
Expand Down Expand Up @@ -809,7 +820,7 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,

useEffect(() => {
if (inVideoPlayer) {
extension.startHeartbeat({ fromVideoPlayer: true, loadedSubtitles: false });
extension.startHeartbeat({ fromVideoPlayer: true, loadedSubtitles: false, syncedVideoElement: undefined });
return undefined;
}

Expand Down Expand Up @@ -884,9 +895,13 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,
}

const unsubscribe = extension.subscribe(onMessage);
extension.startHeartbeat({ fromVideoPlayer: false, loadedSubtitles: subtitles.length > 0 });
extension.startHeartbeat({
fromVideoPlayer: false,
loadedSubtitles: subtitles.length > 0,
syncedVideoElement: tab,
});
return unsubscribe;
}, [extension, subtitles, inVideoPlayer, sources.videoFileUrl, handleFiles, handleAnki, handleUnloadVideo]);
}, [extension, subtitles, inVideoPlayer, sources.videoFileUrl, tab, handleFiles, handleAnki, handleUnloadVideo]);

useEffect(() => {
if (inVideoPlayer) {
Expand Down Expand Up @@ -1005,6 +1020,37 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,

const handleFileSelector = useCallback(() => fileInputRef.current?.click(), []);

const handleVideoElementSelected = useCallback(
async (videoElement: VideoTabModel) => {
const { id: tabId, synced, src } = videoElement;

if (synced) {
const response = (await extension.requestSubtitles(tabId, src)) as RequestSubtitlesResponse | undefined;

if (response !== undefined) {
const { subtitles, subtitleFileNames } = response;

if (subtitleFileNames.length > 0) {
const subtitleFileName = subtitleFileNames[0];
setFileName(subtitleFileName.substring(0, subtitleFileName.lastIndexOf('.')));
const length = subtitles.length > 0 ? subtitles[subtitles.length - 1].end : 0;
setSubtitles(
subtitles.map((s, i) => ({
...s,
displayTime: timeDurationDisplay(s.start, length),
index: i,
}))
);
setTab(videoElement);
}
}
} else {
extension.loadSubtitles(tabId, src);
}
},
[extension]
);

const handleDownloadSubtitleFilesAsSrt = useCallback(async () => {
if (sources.subtitleFiles === undefined) {
return;
Expand Down Expand Up @@ -1127,7 +1173,8 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,

const loading = loadingSources.length !== 0;
const nothingLoaded =
(loading && !videoFrameRef.current) || (sources.subtitleFiles.length === 0 && !sources.videoFile);
tab === undefined &&
((loading && !videoFrameRef.current) || (sources.subtitleFiles.length === 0 && !sources.videoFile));
const appBarHidden = sources.videoFile !== undefined && ((theaterMode && !videoPopOut) || videoFullscreen);
const effectiveCopyHistoryOpen = copyHistoryOpen && !videoFullscreen;

Expand Down Expand Up @@ -1251,7 +1298,9 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,
loading={loading}
dragging={dragging}
appBarHidden={appBarHidden}
videoElements={availableTabs ?? []}
onFileSelector={handleFileSelector}
onVideoElementSelected={handleVideoElementSelected}
/>
)}
<DragOverlay
Expand Down Expand Up @@ -1279,7 +1328,6 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,
onVideoPopOut={handleVideoPopOut}
onPlayModeChangedViaBind={handleAutoPauseModeChangedViaBind}
onSubtitles={setSubtitles}
onTakeScreenshot={handleTakeScreenshot}
tab={tab}
availableTabs={availableTabs ?? []}
sources={sources}
Expand All @@ -1297,7 +1345,6 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged,
disableKeyEvents={disableKeyEvents}
miningContext={miningContext}
keyBinder={keyBinder}
ankiDialogOpen={ankiDialogOpen}
/>
</Content>
</Paper>
Expand Down
84 changes: 53 additions & 31 deletions common/app/components/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Typography from '@material-ui/core/Typography';
import ChromeExtension from '../services/chrome-extension';
import { Theme } from '@material-ui/core/styles';
import { useAppBarHeight } from '../hooks/use-app-bar-height';
import { VideoTabModel } from '../..';
import VideoElementSelector from './VideoElementSelector';

interface StylesProps {
appBarHidden: boolean;
Expand All @@ -31,6 +33,12 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
browseLink: {
cursor: 'pointer',
},
videoElementSelectorContainer: {
position: 'absolute',
bottom: 0,
padding: theme.spacing(2),
width: '100%',
},
}));

interface Props {
Expand All @@ -40,9 +48,11 @@ interface Props {
loading: boolean;
dragging: boolean;
appBarHidden: boolean;
videoElements: VideoTabModel[];
onFileSelector: React.MouseEventHandler<HTMLAnchorElement> &
React.MouseEventHandler<HTMLSpanElement> &
React.MouseEventHandler<HTMLLabelElement>;
onVideoElementSelected: (videoElement: VideoTabModel) => void;
}

export default function LandingPage({
Expand All @@ -52,7 +62,9 @@ export default function LandingPage({
loading,
dragging,
appBarHidden,
videoElements,
onFileSelector,
onVideoElementSelected,
}: Props) {
const appBarHeight = useAppBarHeight();
const classes = useStyles({ appBarHidden, appBarHeight });
Expand All @@ -61,40 +73,50 @@ export default function LandingPage({
return (
<Paper square variant="elevation" elevation={0} className={classes.background}>
<Fade in={!loading && !dragging} timeout={500}>
<Typography variant="h6">
<Trans i18nKey={'landing.cta'}>
Drag and drop subtitle and media files, or
<Link
target="#"
className={classes.browseLink}
onClick={onFileSelector}
color="secondary"
component="label"
>
browse
</Link>
.
</Trans>
<br />
{!extension.installed && (
<Trans i18nKey="landing.extensionNotInstalled">
Install the
<Link color="secondary" target="_blank" rel="noreferrer" href={extensionUrl}>
Chrome extension
<>
<Typography variant="h6">
<Trans i18nKey={'landing.cta'}>
Drag and drop subtitle and media files, or
<Link
target="#"
className={classes.browseLink}
onClick={onFileSelector}
color="secondary"
component="label"
>
browse
</Link>
to sync subtitles with streaming video.
</Trans>
)}
{extensionUpdateAvailable && (
<Trans i18nKey="landing.extensionUpdateAvailable">
An extension
<Link color="secondary" target="_blank" rel="noreferrer" href={extensionUrl}>
update
</Link>{' '}
is available.
.
</Trans>
<br />
{!extension.installed && (
<Trans i18nKey="landing.extensionNotInstalled">
Install the
<Link color="secondary" target="_blank" rel="noreferrer" href={extensionUrl}>
Chrome extension
</Link>
to sync subtitles with streaming video.
</Trans>
)}
{extensionUpdateAvailable && (
<Trans i18nKey="landing.extensionUpdateAvailable">
An extension
<Link color="secondary" target="_blank" rel="noreferrer" href={extensionUrl}>
update
</Link>{' '}
is available.
</Trans>
)}
</Typography>
{extension.supportsLandingPageStreamingVideoElementSelector && videoElements.length > 0 && (
<div className={classes.videoElementSelectorContainer}>
<VideoElementSelector
videoElements={videoElements}
onVideoElementSelected={onVideoElementSelected}
/>
</div>
)}
</Typography>
</>
</Fade>
</Paper>
);
Expand Down
8 changes: 2 additions & 6 deletions common/app/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ interface PlayerProps {
tab?: VideoTabModel;
availableTabs: VideoTabModel[];
miningContext: MiningContext;
ankiDialogOpen: boolean;
origin: string;
onError: (error: any) => void;
onUnloadVideo: (url: string) => void;
Expand All @@ -118,7 +117,6 @@ interface PlayerProps {
onVideoPopOut: () => void;
onPlayModeChangedViaBind: (oldPlayMode: PlayMode, newPlayMode: PlayMode) => void;
onSubtitles: (subtitles: DisplaySubtitleModel[]) => void;
onTakeScreenshot: (mediaTimestamp: number) => void;
disableKeyEvents: boolean;
jumpToSubtitle?: SubtitleModel;
rewindSubtitle?: SubtitleModel;
Expand All @@ -145,21 +143,18 @@ const Player = React.memo(function Player({
tab,
availableTabs,
miningContext,
ankiDialogOpen,
origin,
onError,
onUnloadVideo,
onCopy,
onLoaded,
onTabSelected,
onAnkiDialogRequest,
onAnkiDialogRewind,
onAppBarToggle,
onHideSubtitlePlayer,
onVideoPopOut,
onPlayModeChangedViaBind,
onSubtitles,
onTakeScreenshot,
disableKeyEvents,
jumpToSubtitle,
rewindSubtitle,
Expand Down Expand Up @@ -417,7 +412,8 @@ const Player = React.memo(function Player({
channel === undefined ||
subtitles === undefined ||
subtitlesSentThroughChannel ||
subtitleFiles === undefined
subtitleFiles === undefined ||
subtitleFiles.length === 0
) {
return;
}
Expand Down
69 changes: 69 additions & 0 deletions common/app/components/VideoElementSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useState } from 'react';
import MenuItem from '@material-ui/core/MenuItem';
import TextField from '@material-ui/core/TextField';
import { useTranslation } from 'react-i18next';
import { VideoTabModel } from '../..';
import { useTheme } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

interface Props {
onVideoElementSelected: (element: VideoTabModel) => void;
videoElements: VideoTabModel[];
}

const Favicon = ({ videoElement }: { videoElement: VideoTabModel }) => {
const theme = useTheme();
return (
<>
{videoElement.faviconUrl && (
<img src={videoElement.faviconUrl} style={{ width: 24, marginRight: theme.spacing(1) }} />
)}
</>
);
};

const VideoElementSelector = ({ videoElements, onVideoElementSelected }: Props) => {
const { t } = useTranslation();
const [selectedVideoElement, setSelectedVideoElement] = useState<VideoTabModel>();

if (videoElements.length === 1) {
const videoElement = videoElements[0];
return (
<Button variant="outlined" style={{ width: '100%' }} onClick={() => onVideoElementSelected(videoElement)}>
<Favicon videoElement={videoElement} />
{videoElement.title}
</Button>
);
}

return (
<TextField
select
variant="outlined"
size="small"
color="secondary"
style={{ width: '100%' }}
value={selectedVideoElement?.src ?? ''}
onChange={(e) => {
const element = videoElements.find((v) => v.src === e.target.value);
setSelectedVideoElement(element);

if (element) {
onVideoElementSelected(element);
}
}}
disabled={videoElements.length === 0}
label={t('controls.selectVideoElement')}
helperText={videoElements.length === 0 ? t('landing.noVideoElementsDetected') : undefined}
>
{videoElements.map((v) => (
<MenuItem key={v.src} value={v.src}>
<Favicon videoElement={v} />
{v.title}
</MenuItem>
))}
</TextField>
);
};

export default VideoElementSelector;
Loading

0 comments on commit 8251d03

Please sign in to comment.