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:
Merlin
2025-06-05 05:19:16 +02:00
committed by GitHub
parent 436a641746
commit aa08701049
24 changed files with 332 additions and 107 deletions

View File

@@ -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)}>

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}

View 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;