tubearchivist/frontend/src/pages/Playlist.tsx

397 lines
14 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link, useNavigate, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
import loadPlaylistById, { PlaylistResponseType } from '../api/loader/loadPlaylistById';
import { OutletContextType } from './Base';
import { VideoType } from './Home';
import Filterbar from '../components/Filterbar';
import loadChannelById, { ChannelResponseType } from '../api/loader/loadChannelById';
import VideoList from '../components/VideoList';
import Pagination, { PaginationType } from '../components/Pagination';
import ChannelOverview from '../components/ChannelOverview';
import Linkify from '../components/Linkify';
import {
ViewStyleNames,
ViewStyleNamesType,
ViewStylesEnum,
} from '../configuration/constants/ViewStyle';
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
import deletePlaylist from '../api/actions/deletePlaylist';
import Routes from '../configuration/routes/RouteList';
import formatDate from '../functions/formatDates';
import queueReindex from '../api/actions/queueReindex';
import updateWatchedState from '../api/actions/updateWatchedState';
import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import Button from '../components/Button';
import loadVideoListByFilter, {
VideoTypes,
WatchTypes,
WatchTypesEnum,
} from '../api/loader/loadVideoListByPage';
import useIsAdmin from '../functions/useIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiResponseType } from '../functions/APIClient';
import NotFound from './NotFound';
import updatePlaylistSortOrder from '../api/actions/updatePlaylistSortOrder';
import { useFilterBarTempConf } from '../stores/FilterbarTempConf';
export type VideoResponseType = {
data?: VideoType[];
paginate?: PaginationType;
};
const Playlist = () => {
const { playlistId } = useParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId');
const { userConfig } = useUserConfigStore();
const { filterHeight } = useFilterBarTempConf();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = useIsAdmin();
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const [refresh, setRefresh] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [reindex, setReindex] = useState(false);
const [playlistResponse, setPlaylistResponse] = useState<ApiResponseType<PlaylistResponseType>>();
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
const { data: playlistResponseData, error: playlistResponseError } = playlistResponse ?? {};
const { data: channelResponseData } = channelResponse ?? {};
const { data: videoResponseData } = videoResponse ?? {};
const playlist = playlistResponseData;
const channel = channelResponseData;
const videos = videoResponseData?.data;
const pagination = videoResponseData?.paginate;
const palylistEntries = playlistResponseData?.playlist_entries;
const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
const videoInPlaylistCount = Number(palylistEntries?.length);
const viewStyle = userConfig.view_style_home; // its a list of videos, so view_style_home
const gridItems = userConfig.grid_items;
const isGridView = viewStyle === ViewStylesEnum.Grid;
const gridView = isGridView ? `boxed-${gridItems}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
useEffect(() => {
(async () => {
const playlist = await loadPlaylistById(playlistId);
const video = await loadVideoListByFilter({
playlist: playlistId,
page: currentPage,
watch:
userConfig.hide_watched === null
? null
: ((userConfig.hide_watched
? WatchTypesEnum.Watched
: WatchTypesEnum.Unwatched) as WatchTypes),
type: userConfig.vid_type_filter as VideoTypes,
height: filterHeight,
});
setPlaylistResponse(playlist);
setVideoResponse(video);
const { data: playlistResponseData } = playlist ?? {};
const isCustomPlaylist = playlistResponseData?.playlist_type === 'custom';
if (!isCustomPlaylist) {
const channel = await loadChannelById(playlistResponseData?.playlist_channel_id || '');
setChannelResponse(channel);
}
setRefresh(false);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
playlistId,
userConfig.hide_watched,
userConfig.vid_type_filter,
filterHeight,
refresh,
currentPage,
pagination?.current_page,
videoId,
]);
const errorMessage = playlistResponseError?.error;
if (errorMessage) {
return <NotFound failType="playlist" />;
}
if (!playlist || !playlistId) return [];
const isCustomPlaylist = playlist.playlist_type === 'custom';
return (
<>
<title>{`TA | Playlist: ${playlist.playlist_name}`}</title>
<ScrollToTopOnNavigate />
<div className="boxed-content">
<div className="title-bar">
<h1>{playlist.playlist_name}</h1>
</div>
<div className="info-box info-box-3">
{!isCustomPlaylist && channel && (
<ChannelOverview
channelId={channel?.channel_id}
channelname={channel?.channel_name}
channelSubs={channel?.channel_subs}
channelSubscribed={channel?.channel_subscribed}
channelThumbUrl={channel.channel_thumb_url}
setRefresh={setRefresh}
/>
)}
<div className="info-box-item">
<div>
<p>Last refreshed: {formatDate(playlist.playlist_last_refresh)}</p>
{!isCustomPlaylist && (
<>
<p>
Playlist:
{playlist.playlist_subscribed && (
<>
{isAdmin && (
<Button
label="Unsubscribe"
className="unsubscribe"
type="button"
title={`Unsubscribe from ${playlist.playlist_name}`}
onClick={async () => {
await updatePlaylistSubscription(playlistId, false);
setRefresh(true);
}}
/>
)}
</>
)}{' '}
{!playlist.playlist_subscribed && (
<Button
label="Subscribe"
type="button"
title={`Subscribe to ${playlist.playlist_name}`}
onClick={async () => {
await updatePlaylistSubscription(playlistId, true);
setRefresh(true);
}}
/>
)}
</p>
{playlist.playlist_active && (
<p>
Youtube:{' '}
<a
href={`https://www.youtube.com/playlist?list=${playlist.playlist_id}`}
target="_blank"
rel="noopener noreferrer"
>
Active
</a>
</p>
)}
{!playlist.playlist_active && <p>Youtube: Deactivated</p>}
</>
)}
{!showDeleteConfirm && (
<Button
label="Delete Playlist"
id="delete-item"
onClick={() => setShowDeleteConfirm(!showDeleteConfirm)}
/>
)}
{showDeleteConfirm && (
<div className="delete-confirm">
<span>Delete {playlist.playlist_name}?</span>
<Button
label="Delete metadata"
onClick={async () => {
await deletePlaylist(playlistId, false);
navigate(Routes.Playlists);
}}
/>
<Button
label="Delete all"
className="danger-button"
onClick={async () => {
await deletePlaylist(playlistId, true);
navigate(Routes.Playlists);
}}
/>
<Button label="Cancel" onClick={() => setShowDeleteConfirm(!showDeleteConfirm)} />
</div>
)}
</div>
</div>
<div className="info-box-item">
<div>
{videoArchivedCount > 0 && (
<>
<p>
Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
</p>
<div id="watched-button" className="button-box">
<Button
label="Mark as watched"
title={`Mark all videos from ${playlist.playlist_name} as watched`}
type="button"
onClick={async () => {
await updateWatchedState({
id: playlistId,
is_watched: true,
});
setRefresh(true);
}}
/>{' '}
<Button
label="Mark as unwatched"
title={`Mark all videos from ${playlist.playlist_name} as unwatched`}
type="button"
onClick={async () => {
await updateWatchedState({
id: playlistId,
is_watched: false,
});
setRefresh(true);
}}
/>
</div>
</>
)}
{reindex && <p>Reindex scheduled</p>}
{!reindex && (
<div id="reindex-button" className="button-box">
{!isCustomPlaylist && (
<Button
label="Reindex"
title={`Reindex Playlist ${playlist.playlist_name}`}
onClick={async () => {
setReindex(true);
await queueReindex(playlist.playlist_id, 'playlist');
}}
/>
)}{' '}
<Button
label="Reindex Videos"
title={`Reindex Videos of ${playlist.playlist_name}`}
onClick={async () => {
setReindex(true);
await queueReindex(playlist.playlist_id, 'playlist', true);
}}
/>
</div>
)}
{playlist.playlist_type !== 'custom' && (
<div className="toggle">
<span>Switch sort order:</span>
<div className="toggleBox">
<input
id="playlist_sort_order"
type="checkbox"
checked={playlist.playlist_sort_order === 'bottom'}
onChange={async () => {
const newSortOrder =
playlist.playlist_sort_order === 'top' ? 'bottom' : 'top';
await updatePlaylistSortOrder(playlist.playlist_id, newSortOrder);
setRefresh(true);
}}
/>
{playlist.playlist_sort_order === 'bottom' ? (
<label htmlFor="" className="onbtn">
On
</label>
) : (
<label htmlFor="" className="ofbtn">
Off
</label>
)}
</div>
</div>
)}
</div>
</div>
</div>
{playlist.playlist_description !== 'False' && (
<div className="description-box">
<p
id={descriptionExpanded ? 'text-expand-expanded' : 'text-expand'}
className="description-text"
>
<Linkify>{playlist.playlist_description}</Linkify>
</p>
<Button
label="Show more"
id="text-expand-button"
onClick={() => setDescriptionExpanded(!descriptionExpanded)}
/>
</div>
)}
</div>
<div className={`boxed-content ${gridView}`}>
<Filterbar
viewStyle={ViewStyleNames.Home as ViewStyleNamesType} // its a list of videos, so ViewStyleNames.Home
showSort={false}
showTypeFilter={true}
/>
</div>
<EmbeddableVideoPlayer videoId={videoId} />
<div className={`boxed-content ${gridView}`}>
<div className={`video-list ${viewStyle} ${gridViewGrid}`}>
<VideoList
videoList={videos}
viewStyle={viewStyle}
playlistId={playlistId}
showReorderButton={isCustomPlaylist}
refreshVideoList={setRefresh}
/>
{videoInPlaylistCount === 0 && (
<>
{isCustomPlaylist && (
<p>
Try going to the <a href="{% url 'home' %}">home page</a> to add videos to this
playlist.
</p>
)}
{!isCustomPlaylist && (
<p>
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the
scan and download tasks.
</p>
)}
</>
)}
</div>
</div>
<div className="boxed-content">
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
</div>
</>
);
};
export default Playlist;