mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-08 00:29:31 +00:00
Add video details view (#956)
* Add Video details page to settings * Add option A * Refac remove option A * Refac viewStyleType and viewStyleEnum * Add viewStyle Table * Refac remove unused code * Refac show resolution in one cell
This commit is contained in:
@@ -16,7 +16,7 @@ type ChannelListProps = {
|
||||
|
||||
const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
|
||||
const { userConfig } = useUserConfigStore();
|
||||
const viewLayout = userConfig.view_style_channel;
|
||||
const viewStyle = userConfig.view_style_channel;
|
||||
|
||||
if (!channelList || channelList.length === 0) {
|
||||
return <p>No channels found.</p>;
|
||||
@@ -26,8 +26,8 @@ const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
|
||||
<>
|
||||
{channelList.map(channel => {
|
||||
return (
|
||||
<div key={channel.channel_id} className={`channel-item ${viewLayout}`}>
|
||||
<div className={`channel-banner ${viewLayout}`}>
|
||||
<div key={channel.channel_id} className={`channel-item ${viewStyle}`}>
|
||||
<div className={`channel-banner ${viewStyle}`}>
|
||||
<Link to={Routes.Channel(channel.channel_id)}>
|
||||
<ChannelBanner
|
||||
channelId={channel.channel_id}
|
||||
@@ -35,7 +35,7 @@ const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`info-box info-box-2 ${viewLayout}`}>
|
||||
<div className={`info-box info-box-2 ${viewStyle}`}>
|
||||
<div className="info-box-item">
|
||||
<div className="round-img">
|
||||
<Link to={Routes.Channel(channel.channel_id)}>
|
||||
|
||||
@@ -16,14 +16,14 @@ type DownloadListItemProps = {
|
||||
|
||||
const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||
const { userConfig } = useUserConfigStore();
|
||||
const view = userConfig.view_style_downloads;
|
||||
const viewStyle = userConfig.view_style_downloads;
|
||||
const showIgnored = userConfig.show_ignored_only;
|
||||
|
||||
const [hideDownload, setHideDownload] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={`video-item ${view}`} id={`dl-${download.youtube_id}`}>
|
||||
<div className={`video-thumb-wrap ${view}`}>
|
||||
<div className={`video-item ${viewStyle}`} id={`dl-${download.youtube_id}`}>
|
||||
<div className={`video-thumb-wrap ${viewStyle}`}>
|
||||
<div className="video-thumb">
|
||||
<VideoThumbnail videoThumbUrl={download.vid_thumb_url} />
|
||||
|
||||
@@ -39,7 +39,7 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`video-desc ${view}`}>
|
||||
<div className={`video-desc ${viewStyle}`}>
|
||||
<div>
|
||||
{download.channel_indexed && (
|
||||
<Link to={Routes.Channel(download.channel_id)}>{download.channel_name}</Link>
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import iconSort from '/img/icon-sort.svg';
|
||||
import iconAdd from '/img/icon-add.svg';
|
||||
import iconSubstract from '/img/icon-substract.svg';
|
||||
import iconGridView from '/img/icon-gridview.svg';
|
||||
import iconListView from '/img/icon-listview.svg';
|
||||
import { SortByType, SortOrderType } from '../pages/Home';
|
||||
import iconTableView from '/img/icon-tableview.svg';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { ViewStyles } from '../configuration/constants/ViewStyle';
|
||||
import { ViewStyleNamesType, ViewStylesEnum } from '../configuration/constants/ViewStyle';
|
||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import {
|
||||
SortByEnum,
|
||||
SortByType,
|
||||
SortOrderEnum,
|
||||
SortOrderType,
|
||||
} from '../api/loader/loadVideoListByPage';
|
||||
|
||||
type FilterbarProps = {
|
||||
hideToggleText: string;
|
||||
viewStyleName: string;
|
||||
viewStyle: ViewStyleNamesType;
|
||||
showSort?: boolean;
|
||||
};
|
||||
|
||||
const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: FilterbarProps) => {
|
||||
const Filterbar = ({ hideToggleText, viewStyle, showSort = true }: FilterbarProps) => {
|
||||
const { userConfig, setUserConfig } = useUserConfigStore();
|
||||
|
||||
const [showHidden, setShowHidden] = useState(false);
|
||||
const isGridView = userConfig.view_style_home === ViewStyles.grid;
|
||||
|
||||
const currentViewStyle = userConfig[viewStyle];
|
||||
const isGridView = currentViewStyle === ViewStylesEnum.Grid;
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSort) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentViewStyle === ViewStylesEnum.Table) {
|
||||
setShowHidden(true);
|
||||
} else {
|
||||
setShowHidden(false);
|
||||
}
|
||||
}, [currentViewStyle, showSort]);
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
@@ -55,7 +76,7 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showHidden && showSort && (
|
||||
{showHidden && (
|
||||
<div className="sort">
|
||||
<div id="form">
|
||||
<span>Sort by:</span>
|
||||
@@ -67,12 +88,9 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
|
||||
handleUserConfigUpdate({ sort_by: event.target.value as SortByType });
|
||||
}}
|
||||
>
|
||||
<option value="published">date published</option>
|
||||
<option value="downloaded">date downloaded</option>
|
||||
<option value="views">views</option>
|
||||
<option value="likes">likes</option>
|
||||
<option value="duration">duration</option>
|
||||
<option value="mediasize">media size</option>
|
||||
{Object.entries(SortByEnum).map(([key, value]) => {
|
||||
return <option value={value}>{key}</option>;
|
||||
})}
|
||||
</select>
|
||||
<select
|
||||
name="sort_order"
|
||||
@@ -82,19 +100,20 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
|
||||
handleUserConfigUpdate({ sort_order: event.target.value as SortOrderType });
|
||||
}}
|
||||
>
|
||||
<option value="asc">asc</option>
|
||||
<option value="desc">desc</option>
|
||||
{Object.entries(SortOrderEnum).map(([key, value]) => {
|
||||
return <option value={value}>{key}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="view-icons">
|
||||
{setShowHidden && showSort && (
|
||||
{showSort && (
|
||||
<img
|
||||
src={iconSort}
|
||||
alt="sort-icon"
|
||||
onClick={() => {
|
||||
setShowHidden?.(!showHidden);
|
||||
setShowHidden(!showHidden);
|
||||
}}
|
||||
id="animate-icon"
|
||||
/>
|
||||
@@ -125,17 +144,24 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
|
||||
<img
|
||||
src={iconGridView}
|
||||
onClick={() => {
|
||||
handleUserConfigUpdate({ [viewStyleName]: 'grid' });
|
||||
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.Grid });
|
||||
}}
|
||||
alt="grid view"
|
||||
/>
|
||||
<img
|
||||
src={iconListView}
|
||||
onClick={() => {
|
||||
handleUserConfigUpdate({ [viewStyleName]: 'list' });
|
||||
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.List });
|
||||
}}
|
||||
alt="list view"
|
||||
/>
|
||||
<img
|
||||
src={iconTableView}
|
||||
onClick={() => {
|
||||
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.Table });
|
||||
}}
|
||||
alt="table view"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ type PlaylistListProps = {
|
||||
|
||||
const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
|
||||
const { userConfig } = useUserConfigStore();
|
||||
const viewLayout = userConfig.view_style_playlist;
|
||||
const viewStyle = userConfig.view_style_playlist;
|
||||
|
||||
if (!playlistList || playlistList.length === 0) {
|
||||
return <p>No playlists found.</p>;
|
||||
@@ -24,7 +24,7 @@ const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
|
||||
<>
|
||||
{playlistList.map((playlist: PlaylistType) => {
|
||||
return (
|
||||
<div key={playlist.playlist_id} className={`playlist-item ${viewLayout}`}>
|
||||
<div key={playlist.playlist_id} className={`playlist-item ${viewStyle}`}>
|
||||
<div className="playlist-thumbnail">
|
||||
<Link to={Routes.Playlist(playlist.playlist_id)}>
|
||||
<PlaylistThumbnail
|
||||
@@ -33,7 +33,7 @@ const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`playlist-desc ${viewLayout}`}>
|
||||
<div className={`playlist-desc ${viewStyle}`}>
|
||||
{playlist.playlist_type != 'custom' && (
|
||||
<Link to={Routes.Channel(playlist.playlist_channel_id)}>
|
||||
<h3>{playlist.playlist_channel}</h3>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { VideoType, ViewLayoutType } from '../pages/Home';
|
||||
import { ViewStylesEnum, ViewStylesType } from '../configuration/constants/ViewStyle';
|
||||
import { VideoType } from '../pages/Home';
|
||||
import VideoListItem from './VideoListItem';
|
||||
import VideoListItemTable from './VideoListItemTable';
|
||||
|
||||
type VideoListProps = {
|
||||
videoList: VideoType[] | undefined;
|
||||
viewLayout: ViewLayoutType;
|
||||
viewStyle: ViewStylesType;
|
||||
playlistId?: string;
|
||||
showReorderButton?: boolean;
|
||||
refreshVideoList: (refresh: boolean) => void;
|
||||
@@ -11,7 +13,7 @@ type VideoListProps = {
|
||||
|
||||
const VideoList = ({
|
||||
videoList,
|
||||
viewLayout,
|
||||
viewStyle,
|
||||
playlistId,
|
||||
showReorderButton = false,
|
||||
refreshVideoList,
|
||||
@@ -20,6 +22,10 @@ const VideoList = ({
|
||||
return <p>No videos found.</p>;
|
||||
}
|
||||
|
||||
if (viewStyle === ViewStylesEnum.Table) {
|
||||
return <VideoListItemTable videoList={videoList} viewStyle={viewStyle} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{videoList.map(video => {
|
||||
@@ -27,7 +33,7 @@ const VideoList = ({
|
||||
<VideoListItem
|
||||
key={video.youtube_id}
|
||||
video={video}
|
||||
viewLayout={viewLayout}
|
||||
viewStyle={viewStyle}
|
||||
playlistId={playlistId}
|
||||
showReorderButton={showReorderButton}
|
||||
refreshVideoList={refreshVideoList}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
import { VideoType, ViewLayoutType } from '../pages/Home';
|
||||
import { VideoType } from '../pages/Home';
|
||||
import iconPlay from '/img/icon-play.svg';
|
||||
import iconDotMenu from '/img/icon-dot-menu.svg';
|
||||
import iconClose from '/img/icon-close.svg';
|
||||
@@ -11,10 +11,11 @@ import MoveVideoMenu from './MoveVideoMenu';
|
||||
import { useState } from 'react';
|
||||
import deleteVideoProgressById from '../api/actions/deleteVideoProgressById';
|
||||
import VideoThumbnail from './VideoThumbail';
|
||||
import { ViewStylesType } from '../configuration/constants/ViewStyle';
|
||||
|
||||
type VideoListItemProps = {
|
||||
video: VideoType;
|
||||
viewLayout: ViewLayoutType;
|
||||
viewStyle: ViewStylesType;
|
||||
playlistId?: string;
|
||||
showReorderButton?: boolean;
|
||||
refreshVideoList: (refresh: boolean) => void;
|
||||
@@ -22,7 +23,7 @@ type VideoListItemProps = {
|
||||
|
||||
const VideoListItem = ({
|
||||
video,
|
||||
viewLayout,
|
||||
viewStyle,
|
||||
playlistId,
|
||||
showReorderButton = false,
|
||||
refreshVideoList,
|
||||
@@ -36,7 +37,7 @@ const VideoListItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`video-item ${viewLayout}`}>
|
||||
<div className={`video-item ${viewStyle}`}>
|
||||
<a
|
||||
onClick={() => {
|
||||
setSearchParams(params => {
|
||||
@@ -46,7 +47,7 @@ const VideoListItem = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className={`video-thumb-wrap ${viewLayout}`}>
|
||||
<div className={`video-thumb-wrap ${viewStyle}`}>
|
||||
<div className="video-thumb">
|
||||
<VideoThumbnail videoThumbUrl={video.vid_thumb_url} />
|
||||
|
||||
@@ -72,7 +73,7 @@ const VideoListItem = ({
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div className={`video-desc ${viewLayout}`}>
|
||||
<div className={`video-desc ${viewStyle}`}>
|
||||
<div className="video-desc-player" id={`video-info-${video.youtube_id}`}>
|
||||
<WatchedCheckBox
|
||||
watched={video.player.watched}
|
||||
|
||||
64
frontend/src/components/VideoListItemTable.tsx
Normal file
64
frontend/src/components/VideoListItemTable.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
import { VideoType } from '../pages/Home';
|
||||
import { ViewStylesType } from '../configuration/constants/ViewStyle';
|
||||
import humanFileSize from '../functions/humanFileSize';
|
||||
import { FileSizeUnits } from '../api/actions/updateUserConfig';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
|
||||
type VideoListItemProps = {
|
||||
videoList: VideoType[] | undefined;
|
||||
viewStyle: ViewStylesType;
|
||||
};
|
||||
|
||||
const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => {
|
||||
const { userConfig } = useUserConfigStore();
|
||||
|
||||
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
||||
|
||||
return (
|
||||
<div className={`video-item ${viewStyle}`}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th>Title</th>
|
||||
<th>Type</th>
|
||||
<th>Resolution</th>
|
||||
<th>Media size</th>
|
||||
<th>Video codec</th>
|
||||
<th>Video bitrate</th>
|
||||
<th>Audio codec</th>
|
||||
<th>Audio bitrate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{videoList?.map(({ youtube_id, title, channel, vid_type, media_size, streams }) => {
|
||||
const [videoStream, audioStream] = streams;
|
||||
|
||||
return (
|
||||
<tr key={youtube_id}>
|
||||
<td className="no-nowrap">
|
||||
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
|
||||
</td>
|
||||
<td className="no-nowrap title">
|
||||
<Link to={Routes.Video(youtube_id)}>{title}</Link>
|
||||
</td>
|
||||
<td>{vid_type}</td>
|
||||
<td>{`${videoStream.width}x${videoStream.height}`}</td>
|
||||
<td>{humanFileSize(media_size, useSiUnits)}</td>
|
||||
<td>{videoStream.codec}</td>
|
||||
<td>{humanFileSize(videoStream.bitrate, useSiUnits)}</td>
|
||||
<td>{audioStream.codec}</td>
|
||||
<td>{humanFileSize(audioStream.bitrate, useSiUnits)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoListItemTable;
|
||||
Reference in New Issue
Block a user