tubearchivist/frontend/src/pages/Playlist.tsx

380 lines
13 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link, useNavigate, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
import loadPlaylistById from '../api/loader/loadPlaylistById';
import { OutletContextType } from './Base';
import { ConfigType, VideoType, ViewLayoutType } from './Home';
import Filterbar from '../components/Filterbar';
import { PlaylistEntryType } from './Playlists';
import loadChannelById 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, ViewStyles } from '../configuration/constants/ViewStyle';
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
import deletePlaylist from '../api/actions/deletePlaylist';
import Routes from '../configuration/routes/RouteList';
import { ChannelResponseType } from './ChannelBase';
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 from '../api/loader/loadVideoListByPage';
import loadIsAdmin from '../functions/getIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
export type PlaylistType = {
playlist_active: boolean;
playlist_channel: string;
playlist_channel_id: string;
playlist_description: string;
playlist_entries: PlaylistEntryType[];
playlist_id: string;
playlist_last_refresh: string;
playlist_name: string;
playlist_subscribed: boolean;
playlist_thumbnail: string;
playlist_type: string;
_index: string;
_score: number;
};
export type PlaylistResponseType = {
data?: PlaylistType;
config?: ConfigType;
};
export type VideoResponseType = {
data?: VideoType[];
config?: ConfigType;
paginate?: PaginationType;
};
const Playlist = () => {
const { playlistId } = useParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId');
const { userConfig } = useUserConfigStore();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = loadIsAdmin();
const userMeConfig = userConfig.config;
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const [refresh, setRefresh] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [reindex, setReindex] = useState(false);
const [playlistResponse, setPlaylistResponse] = useState<PlaylistResponseType>();
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
const playlist = playlistResponse?.data;
const channel = channelResponse?.data;
const videos = videoResponse?.data;
const pagination = videoResponse?.paginate;
const palylistEntries = playlistResponse?.data?.playlist_entries;
const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
const videoInPlaylistCount = pagination?.total_hits;
const showEmbeddedVideo = videoId !== null;
const isGridView = view === ViewStyles.grid;
const gridView = isGridView ? `boxed-${gridItems}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
useEffect(() => {
(async () => {
if (
refresh ||
pagination?.current_page === undefined ||
currentPage !== pagination?.current_page
) {
const playlist = await loadPlaylistById(playlistId);
const video = await loadVideoListByFilter({
playlist: playlistId,
page: currentPage,
watch: hideWatched ? 'unwatched' : undefined,
sort: 'downloaded', // downloaded or published? or playlist sort order?
});
const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
if (!isCustomPlaylist) {
const channel = await loadChannelById(playlist.data.playlist_channel_id);
setChannelResponse(channel);
}
setPlaylistResponse(playlist);
setVideoResponse(video);
setRefresh(false);
}
})();
// Do not add hideWatched this will not work as expected!
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playlistId, refresh, currentPage, pagination?.current_page]);
if (!playlistId || !playlist) {
return `Playlist ${playlistId} not found!`;
}
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"
>
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" id="delete-button">
<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);
}}
/>
<br />
<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>
)}
</div>
</div>
</div>
{playlist.playlist_description && (
<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
hideToggleText="Hide watched videos:"
hideWatched={hideWatched}
isGridView={isGridView}
view={view}
gridItems={gridItems}
userMeConfig={userMeConfig}
setHideWatched={setHideWatched}
setView={setView}
setGridItems={setGridItems}
viewStyleName={ViewStyleNames.playlist}
setRefresh={setRefresh}
/>
</div>
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
<div className={`boxed-content ${gridView}`}>
<div className={`video-list ${view} ${gridViewGrid}`}>
{videoInPlaylistCount === 0 && (
<>
<h2>No videos found...</h2>
{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>
)}
</>
)}
{videoInPlaylistCount !== 0 && (
<VideoList
videoList={videos}
viewLayout={view}
playlistId={playlistId}
showReorderButton={isCustomPlaylist}
refreshVideoList={setRefresh}
/>
)}
</div>
</div>
<div className="boxed-content">
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
</div>
</>
);
};
export default Playlist;