Refac react frontend (#790)

* Add channel config endpoint

* Add channel aggs

* Add playlist show subscribed only toggle

* Fix refresh always on filterbar toggle

* Add loadingindicator for watchstate change

* Fix missing space in scheduling

* Add schedule request and apprisenotifcation

* Refac upgrade TypeScript target to include 2024 for Object.groupBy

* WIP: Schedule page

* WIP: Schedule page

* Add schedule management ( - notification )

* Fix missing space

* Refac show current selection in input

* Add apprise notifictation url

* Add Stream & Shorts channel pages

* Refac autotarget input on search page

* Fix input requiring 1 instead of 0

* Fix remove unused function

* Chore: npm audit fix

* Refac get channel_overwrites from channelById

* Refac remove defaultvalues form select

* Fix delay content refresh to allow the backend to update subscribed state

* Fix styling selection

* Fix lint

* Fix spelling

* Fix remove unused import

* Chore: update all dependencies - React 19 & vite 6

* Add missing property to ValidatedCookieType

* Refac fix complaints about JSX.Element, used ReactNode instead

* Refac remove unused dependency

* Refac replace react-helmet with react 19 implementation

* Fix Application Settings page

* Chore update dependencies

* Add simple playlist autoplay feature

* Refac use server provided channel images path

* Refac use server provided playlistthumbnail images path

* Add save and restore video playback speed
This commit is contained in:
Merlin
2024-12-22 15:59:30 +01:00
committed by GitHub
parent 75339e479e
commit 5a5d47da9b
54 changed files with 10047 additions and 9526 deletions

View File

@@ -1,211 +1,216 @@
import { useEffect, useState } from 'react';
import {
Link,
useLoaderData,
useOutletContext,
useParams,
useSearchParams,
} from 'react-router-dom';
import { SortByType, SortOrderType, ViewLayoutType } from './Home';
import { OutletContextType } from './Base';
import { UserMeType } from '../api/actions/updateUserConfig';
import VideoList from '../components/VideoList';
import Routes from '../configuration/routes/RouteList';
import Pagination from '../components/Pagination';
import Filterbar from '../components/Filterbar';
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
import ChannelOverview from '../components/ChannelOverview';
import loadChannelById from '../api/loader/loadChannelById';
import { ChannelResponseType } from './ChannelBase';
import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import updateWatchedState from '../api/actions/updateWatchedState';
import { Helmet } from 'react-helmet';
import Button from '../components/Button';
import loadVideoListByFilter, {
VideoListByFilterResponseType,
} from '../api/loader/loadVideoListByPage';
type ChannelParams = {
channelId: string;
};
type ChannelVideoLoaderType = {
userConfig: UserMeType;
};
const ChannelVideo = () => {
const { channelId } = useParams() as ChannelParams;
const { userConfig } = useLoaderData() as ChannelVideoLoaderType;
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId');
const userMeConfig = userConfig.config;
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
const [sortBy, setSortBy] = useState<SortByType>(userMeConfig.sort_by || 'published');
const [sortOrder, setSortOrder] = useState<SortOrderType>(userMeConfig.sort_order || 'asc');
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
const [refresh, setRefresh] = useState(false);
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
const channel = channelResponse?.data;
const videoList = videoResponse?.data;
const pagination = videoResponse?.paginate;
const hasVideos = videoResponse?.data?.length !== 0;
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 channelResponse = await loadChannelById(channelId);
const videos = await loadVideoListByFilter({
channel: channelId,
page: currentPage,
watch: hideWatched ? 'unwatched' : undefined,
sort: sortBy,
order: sortOrder,
});
setChannelResponse(channelResponse);
setVideoReponse(videos);
setRefresh(false);
}
})();
// Do not add sort, order, hideWatched this will not work as expected!
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refresh, currentPage, channelId, pagination?.current_page]);
const aggs = {
total_items: { value: '<debug>' },
total_duration: { value_str: '<debug>' },
total_size: { value: '<debug>' },
};
if (!channel) {
return (
<div className="boxed-content">
<br />
<h2>Channel {channelId} not found!</h2>
</div>
);
}
return (
<>
<Helmet>
<title>TA | Channel: {channel.channel_name}</title>
</Helmet>
<ScrollToTopOnNavigate />
<div className="boxed-content">
<div className="info-box info-box-2">
<ChannelOverview
channelId={channel.channel_id}
channelname={channel.channel_name}
channelSubs={channel.channel_subs}
channelSubscribed={channel.channel_subscribed}
showSubscribeButton={true}
isUserAdmin={isAdmin}
setRefresh={setRefresh}
/>
<div className="info-box-item">
{aggs && (
<>
<p>
{aggs.total_items.value} videos <span className="space-carrot">|</span>{' '}
{aggs.total_duration.value_str} playback <span className="space-carrot">|</span>{' '}
Total size {aggs.total_size.value}
</p>
<div className="button-box">
<Button
label="Mark as watched"
id="watched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as watched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: true,
});
setRefresh(true);
}}
/>{' '}
<Button
label="Mark as unwatched"
id="unwatched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as unwatched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: false,
});
setRefresh(true);
}}
/>
</div>
</>
)}
</div>
</div>
</div>
<div className={`boxed-content ${gridView}`}>
<Filterbar
hideToggleText={'Hide watched videos:'}
view={view}
isGridView={isGridView}
hideWatched={hideWatched}
gridItems={gridItems}
sortBy={sortBy}
sortOrder={sortOrder}
userMeConfig={userMeConfig}
setSortBy={setSortBy}
setSortOrder={setSortOrder}
setHideWatched={setHideWatched}
setView={setView}
setGridItems={setGridItems}
viewStyleName={ViewStyleNames.channel}
setRefresh={setRefresh}
/>
</div>
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
<div className={`boxed-content ${gridView}`}>
<div className={`video-list ${view} ${gridViewGrid}`}>
{!hasVideos && (
<>
<h2>No videos found...</h2>
<p>
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the scan
and download tasks.
</p>
</>
)}
<VideoList videoList={videoList} viewLayout={view} refreshVideoList={setRefresh} />
</div>
</div>
{pagination && (
<div className="boxed-content">
<Pagination pagination={pagination} setPage={setCurrentPage} />
</div>
)}
</>
);
};
export default ChannelVideo;
import { useEffect, useState } from 'react';
import {
Link,
useLoaderData,
useOutletContext,
useParams,
useSearchParams,
} from 'react-router-dom';
import { SortByType, SortOrderType, ViewLayoutType } from './Home';
import { OutletContextType } from './Base';
import { UserMeType } from '../api/actions/updateUserConfig';
import VideoList from '../components/VideoList';
import Routes from '../configuration/routes/RouteList';
import Pagination from '../components/Pagination';
import Filterbar from '../components/Filterbar';
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
import ChannelOverview from '../components/ChannelOverview';
import loadChannelById from '../api/loader/loadChannelById';
import { ChannelResponseType } from './ChannelBase';
import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import updateWatchedState from '../api/actions/updateWatchedState';
import Button from '../components/Button';
import loadVideoListByFilter, {
VideoListByFilterResponseType,
VideoTypes,
} from '../api/loader/loadVideoListByPage';
import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
import humanFileSize from '../functions/humanFileSize';
type ChannelParams = {
channelId: string;
};
type ChannelVideoLoaderType = {
userConfig: UserMeType;
};
type ChannelVideoProps = {
videoType: VideoTypes;
};
const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
const { channelId } = useParams() as ChannelParams;
const { userConfig } = useLoaderData() as ChannelVideoLoaderType;
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId');
const userMeConfig = userConfig.config;
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
const [sortBy, setSortBy] = useState<SortByType>(userMeConfig.sort_by || 'published');
const [sortOrder, setSortOrder] = useState<SortOrderType>(userMeConfig.sort_order || 'asc');
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
const [refresh, setRefresh] = useState(false);
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
const [videoAggsResponse, setVideoAggsResponse] = useState<ChannelAggsType>();
const channel = channelResponse?.data;
const videoList = videoResponse?.data;
const pagination = videoResponse?.paginate;
const hasVideos = videoResponse?.data?.length !== 0;
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 channelResponse = await loadChannelById(channelId);
const videos = await loadVideoListByFilter({
channel: channelId,
page: currentPage,
watch: hideWatched ? 'unwatched' : undefined,
sort: sortBy,
order: sortOrder,
type: videoType,
});
const channelAggs = await loadChannelAggs(channelId);
setChannelResponse(channelResponse);
setVideoReponse(videos);
setVideoAggsResponse(channelAggs);
setRefresh(false);
}
})();
// Do not add sort, order, hideWatched this will not work as expected!
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refresh, currentPage, channelId, pagination?.current_page]);
if (!channel) {
return (
<div className="boxed-content">
<br />
<h2>Channel {channelId} not found!</h2>
</div>
);
}
return (
<>
<title>{`TA | Channel: ${channel.channel_name}`}</title>
<ScrollToTopOnNavigate />
<div className="boxed-content">
<div className="info-box info-box-2">
<ChannelOverview
channelId={channel.channel_id}
channelname={channel.channel_name}
channelSubs={channel.channel_subs}
channelSubscribed={channel.channel_subscribed}
channelThumbUrl={channel.channel_thumb_url}
showSubscribeButton={true}
isUserAdmin={isAdmin}
setRefresh={setRefresh}
/>
<div className="info-box-item">
{videoAggsResponse && (
<>
<p>
{videoAggsResponse.total_items.value} videos{' '}
<span className="space-carrot">|</span>{' '}
{videoAggsResponse.total_duration.value_str} playback{' '}
<span className="space-carrot">|</span> Total size{' '}
{humanFileSize(videoAggsResponse.total_size.value, true)}
</p>
<div className="button-box">
<Button
label="Mark as watched"
id="watched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as watched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: true,
});
setRefresh(true);
}}
/>{' '}
<Button
label="Mark as unwatched"
id="unwatched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as unwatched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: false,
});
setRefresh(true);
}}
/>
</div>
</>
)}
</div>
</div>
</div>
<div className={`boxed-content ${gridView}`}>
<Filterbar
hideToggleText={'Hide watched videos:'}
view={view}
isGridView={isGridView}
hideWatched={hideWatched}
gridItems={gridItems}
sortBy={sortBy}
sortOrder={sortOrder}
userMeConfig={userMeConfig}
setSortBy={setSortBy}
setSortOrder={setSortOrder}
setHideWatched={setHideWatched}
setView={setView}
setGridItems={setGridItems}
viewStyleName={ViewStyleNames.channel}
setRefresh={setRefresh}
/>
</div>
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
<div className={`boxed-content ${gridView}`}>
<div className={`video-list ${view} ${gridViewGrid}`}>
{!hasVideos && (
<>
<h2>No videos found...</h2>
<p>
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the scan
and download tasks.
</p>
</>
)}
<VideoList videoList={videoList} viewLayout={view} refreshVideoList={setRefresh} />
</div>
</div>
{pagination && (
<div className="boxed-content">
<Pagination pagination={pagination} setPage={setCurrentPage} />
</div>
)}
</>
);
};
export default ChannelVideo;