diff --git a/app/src/components/VideoPlayer.js b/app/src/components/VideoPlayer.js
index c0604528..0e88cdc9 100644
--- a/app/src/components/VideoPlayer.js
+++ b/app/src/components/VideoPlayer.js
@@ -1,4 +1,4 @@
-import React, {useContext, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {useHotkeys} from 'react-hotkeys-hook';
import {deleteFileGroups, downloadVideoMetadata, tagFileGroup, untagFileGroup} from "../api";
import {Link, useNavigate, useParams} from "react-router";
@@ -17,17 +17,14 @@ import {
useMediaSession,
useTitle
} from "./Common";
-import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
-import Container from "semantic-ui-react/dist/commonjs/elements/Container";
import {VideoPlaceholder} from "./Placeholder";
import {useChannel, useVideoCaptions, useVideoExtras} from "../hooks/customHooks";
-import {ThemeContext} from "../contexts/contexts";
-import {Button, darkTheme, Header, Icon, Loader, Segment, Tab} from "./Theme";
+import {Button, Grid, Header, Icon, Label, Loading, Panel, Tabs, TextInput} from "./ui";
import {VideoCard} from "./Videos";
import {TagsSelector} from "../Tags";
import {AddToPlaylistButton} from "./AddToPlaylist";
-import {Comment, CommentGroup, Input, Label, Transition} from "semantic-ui-react";
import {TaggedDeleteConfirmModal} from "./TaggedDeleteConfirmModal";
+import {IconPlayerPause, IconPlayerPlay} from "@tabler/icons-react";
const MEDIA_PATH = '/media';
@@ -117,7 +114,7 @@ const NoComments = ({video}) => {
No comments have been been downloaded. Refresh the video?
Refresh
@@ -132,43 +129,43 @@ const NoComments = ({video}) => {
}
}
+// Semantic's / have no equivalent in the token library -- this is a nested
+// comment thread, not a reusable widget, so it is built here from plain elements and tokens.
const VideoComment = ({comment, children}) => {
- const {t} = React.useContext(ThemeContext);
-
const {is_favorited, like_count, author, timestamp, text, author_is_uploader} = comment;
// Author comments and favorited comments are important.
let specialIcon = null;
if (author_is_uploader) {
- specialIcon = ;
+ specialIcon = ;
} else if (is_favorited) {
- specialIcon = ;
+ specialIcon = ;
}
- const dateElm = {isoDatetimeToAgoPopup(timestamp * 1000)}
;
- const likesElm = like_count &&
- {humanNumber(comment['like_count'])}
-
;
-
- return
-
-
- {specialIcon}{author}
-
-
- {dateElm}
- {likesElm}
-
-
-
-
-
+ return
+
+ {specialIcon}{author}
+
+
+
{isoDatetimeToAgoPopup(timestamp * 1000)}
+ {like_count ?
+ {humanNumber(comment['like_count'])}
+
: null}
+
+
+
+
{children && !_.isEmpty(children) &&
-
+
{children.map((i, key) => )}
-
+
}
-
+
}
@@ -202,9 +199,9 @@ const Comments = ({comments, video}) => {
groupedByParent[obj.parent] = _.sortBy(children, ['timestamp']);
})
- return
+ return
{rootComments.map((i, key) => )}
-
+
}
// Generate a video page URL for a file object.
@@ -214,11 +211,10 @@ function getVideoLink(file) {
}
function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
- const {theme} = useContext(ThemeContext);
-
const navigate = useNavigate();
const videoRef = React.useRef();
const [taggedFileGroups, setTaggedFileGroups] = useState(null);
+ const [activeTab, setActiveTab] = useState('comments');
useTitle(videoFile ? videoFile.title ? videoFile.title : videoFile.name : null);
let video;
@@ -353,26 +349,9 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
description = formatVideoDescription(description, setVideoTime)
}
- const descriptionPane = {
- menuItem: 'Description', render: () =>
-
- {description}
-
-
- };
-
- const captionsPane = {
- menuItem: 'Captions', render: () =>
- };
-
const getFile = (suffix) => videoFile.files.find(i => i.path.toLowerCase().endsWith(suffix));
const fileSize = (file) => file && file.size
- ? ({humanFileSize(file.size)})
+ ? ({humanFileSize(file.size)})
: null;
const {poster_file, info_json_file} = video;
@@ -380,120 +359,6 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
const ffprobeJsonFile = getFile('.ffprobe.json');
const tableStyle = {width: '100%', borderCollapse: 'separate', borderSpacing: '0 0.7em'};
const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'};
- const filesPane = {
- menuItem: 'Files', render: () =>
-
-
-
- {isAudio ? 'Audio File' : 'Video File'}
-
- {video['video_path']
- ? <>
- {video['video_path']}
- {fileSize(videoPathFile)}>
- : 'Unknown'}
-
-
-
-
- Caption Files
-
- {caption_files
- ? caption_files.map(i =>
-
{i['path']}
- {fileSize(i)}
-
)
- : 'No caption files'}
-
-
-
-
- Poster File
-
- {poster_file
- ? <>
- {poster_file['path']}
- {fileSize(poster_file)}>
- : 'No poster file'}
-
-
-
-
- Info JSON File
-
- {info_json_file
- ? <>
- {info_json_file['path']}
- {fileSize(info_json_file)}>
- : 'No yt-dlp info json file'}
-
-
-
-
- ffprobe JSON File
-
- {ffprobeJsonFile
- ? <>
- {ffprobeJsonFile['path']}
- {fileSize(ffprobeJsonFile)}>
- : 'No ffprobe json file'}
-
-
-
-
- Directory
-
-
-
-
-
- }
-
- const commentsPane = {
- menuItem: 'Comments', render: () =>
-
-
- ,
- };
-
- const tabPanes = [commentsPane, descriptionPane, filesPane, captionsPane];
- const tabMenu = theme === darkTheme ? {inverted: true, attached: true} : {attached: true};
-
- const aboutSegment =
-
-
-
-
-
- Size
- {videoFile.size ? humanFileSize(videoFile.size) : 'Unknown'}
-
-
- View Count
- {video.view_count ? humanNumber(video.view_count) : 'N/A'}
-
-
-
-
- Codec Names
- <>{video.codec_names ? video.codec_names.map(i => {i} ) : 'N/A'}>
-
-
- Censored
- {videoFile.censored ? 'Yes' : 'No'}
-
-
-
-
- Source URL
- {videoFile.url ? {videoFile.url} : 'N/A'}
-
-
-
- ;
const localAddTag = async (name) => {
await tagFileGroup(videoFile, name);
@@ -508,20 +373,18 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
let videoSource = ;
const isAudio = videoFile.mimetype && videoFile.mimetype.startsWith('audio/');
- let prevNextVideosSegment = No related videos found.
;
+ let prevNextVideosPanel = No related videos found.
;
if (prevFile || nextFile) {
- prevNextVideosSegment =
-
-
-
- {prevFile && <>>}
-
-
- {nextFile && <>>}
-
-
+ prevNextVideosPanel =
+
+
+ {prevFile && <>>}
+
+
+ {nextFile && <>>}
+
-
+
}
let title = video.video_path;
@@ -531,19 +394,35 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
title = videoFile.stem;
}
+ const audioPlayPauseLabel = audioPlaying ? 'Pause' : 'Play';
+
return <>
-
+
-
+
{isAudio ? <>
{posterUrl &&
inside is already a filtered leaf on its own.
+ className='audio-poster-container media'
+ role='button'
+ tabIndex={0}
+ aria-label={audioPlaying ? 'Pause audio' : 'Play audio'}
onClick={() => {
if (videoRef.current) {
videoRef.current.paused ? videoRef.current.play() : videoRef.current.pause();
}
}}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ if (videoRef.current) {
+ videoRef.current.paused ? videoRef.current.play() : videoRef.current.pause();
+ }
+ }
+ }}
onMouseEnter={() => setAudioPosterHover(true)}
onMouseLeave={() => setAudioPosterHover(false)}
>
@@ -552,16 +431,24 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
alt={mediaTitle || 'Audio thumbnail'}
style={{maxWidth: '100%', maxHeight: '400px', cursor: 'pointer'}}
/>
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
}
)}
}
-
-
+
+
{videoFile.published_datetime && {isoDatetimeToAgoPopup(videoFile.published_datetime)} }
@@ -597,37 +484,158 @@ function VideoPage({videoFile, prevFile, nextFile, fetchVideo, ...props}) {
-
-
+
Download
await handleDeleteVideo(videoFile.id)}
obeyWROLMode={true}
>Delete
Refresh
-
+
-
+
-
+
- {aboutSegment}
+
+
-
+
+
+ Size
+ {videoFile.size ? humanFileSize(videoFile.size) : 'Unknown'}
+
+
+ View Count
+ {video.view_count ? humanNumber(video.view_count) : 'N/A'}
+
+
+ Codec Names
+ <>{video.codec_names ? video.codec_names.map(i => {i} ) : 'N/A'}>
+
+
+ Censored
+ {videoFile.censored ? 'Yes' : 'No'}
+
+
+ Source URL
+ {videoFile.url ? {videoFile.url} : 'N/A'}
+
+
+
- {prevNextVideosSegment}
-
+
+
+ Comments
+ Description
+ Files
+ Captions
+
+
+
+
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+
+ {isAudio ? 'Audio File' : 'Video File'}
+
+ {video['video_path']
+ ? <>
+ {video['video_path']}
+ {fileSize(videoPathFile)}>
+ : 'Unknown'}
+
+
+
+
+ Caption Files
+
+ {caption_files
+ ? caption_files.map(i =>
+
{i['path']}
+ {fileSize(i)}
+
)
+ : 'No caption files'}
+
+
+
+
+ Poster File
+
+ {poster_file
+ ? <>
+ {poster_file['path']}
+ {fileSize(poster_file)}>
+ : 'No poster file'}
+
+
+
+
+ Info JSON File
+
+ {info_json_file
+ ? <>
+ {info_json_file['path']}
+ {fileSize(info_json_file)}>
+ : 'No yt-dlp info json file'}
+
+
+
+
+ ffprobe JSON File
+
+ {ffprobeJsonFile
+ ? <>
+ {ffprobeJsonFile['path']}
+ {fileSize(ffprobeJsonFile)}>
+ : 'No ffprobe json file'}
+
+
+
+
+ Directory
+
+
+
+
+
+
+
+
+
+
+
+ {prevNextVideosPanel}
+
;
+ return ;
}
if (!captions || captions.length === 0) {
- return No captions available. ;
+ return No captions available.
;
}
let filtered = captions;
@@ -677,14 +685,13 @@ function CaptionsPane({captions, captionsLoading, fetchCaptions, setVideoTime})
});
}
- return
-
+ }
placeholder='Filter captions...'
value={filter}
- onChange={(e, {value}) => setFilter(value)}
- style={{marginBottom: '1em'}}
+ onChange={(e) => setFilter(e.currentTarget.value)}
+ style={{marginBottom: '1em', width: '100%'}}
/>
{filtered.map((chunk, idx) => (
@@ -694,7 +701,7 @@ function CaptionsPane({captions, captionsLoading, fetchCaptions, setVideoTime})
e.preventDefault();
setVideoTime(chunk.start_seconds);
}}
- style={{whiteSpace: 'nowrap', fontFamily: 'monospace'}}
+ style={{whiteSpace: 'nowrap', fontFamily: 'var(--font-mono)'}}
>
{formatCaptionTimestamp(chunk.start_seconds)}
@@ -702,7 +709,7 @@ function CaptionsPane({captions, captionsLoading, fetchCaptions, setVideoTime})
))}
- ;
+ >;
}
export default VideoPage;