Add Video details page to settings

This commit is contained in:
MerlinScheurer 2025-05-10 14:09:59 +02:00
parent 1d727a1170
commit 776481c513
11 changed files with 305 additions and 14 deletions

View File

@ -31,6 +31,8 @@ class SortEnum(enum.Enum):
LIKES = "stats.like_count" LIKES = "stats.like_count"
DURATION = "player.duration" DURATION = "player.duration"
MEDIASIZE = "media_size" MEDIASIZE = "media_size"
WIDTH = "streams.width"
HEIGHT = "streams.height"
@classmethod @classmethod
def values(cls) -> list[str]: def values(cls) -> list[str]:

View File

@ -1,5 +1,6 @@
import { SortByType, SortOrderType, ViewLayoutType } from '../../pages/Home'; import { ViewLayoutType } from '../../pages/Home';
import APIClient from '../../functions/APIClient'; import APIClient from '../../functions/APIClient';
import { SortByType, SortOrderType } from '../loader/loadVideoListByPage';
export type ColourVariants = export type ColourVariants =
| 'dark.css' | 'dark.css'

View File

@ -1,4 +1,4 @@
import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home'; import { ConfigType, VideoType } from '../../pages/Home';
import { PaginationType } from '../../components/Pagination'; import { PaginationType } from '../../components/Pagination';
import APIClient from '../../functions/APIClient'; import APIClient from '../../functions/APIClient';
@ -8,9 +8,45 @@ export type VideoListByFilterResponseType = {
paginate?: PaginationType; paginate?: PaginationType;
}; };
type WatchTypes = 'watched' | 'unwatched' | 'continue'; export type SortByType =
| 'published'
| 'downloaded'
| 'views'
| 'likes'
| 'duration'
| 'mediasize'
| 'width'
| 'height';
export const SortByEnum = {
Published: 'published',
Downloaded: 'downloaded',
Views: 'views',
Likes: 'likes',
Duration: 'duration',
'Media Size': 'mediasize',
};
export const SortByExpandedEnum = {
...SortByEnum,
Width: 'width',
Height: 'height',
};
export type SortOrderType = 'asc' | 'desc';
export const SortOrderEnum = {
Asc: 'asc',
Desc: 'desc',
};
export type VideoTypes = 'videos' | 'streams' | 'shorts'; export type VideoTypes = 'videos' | 'streams' | 'shorts';
export type WatchTypes = 'watched' | 'unwatched' | 'continue';
export const WatchTypesEnum = {
Watched: 'watched',
Unwatched: 'unwatched',
Continue: 'continue',
};
type FilterType = { type FilterType = {
page?: number; page?: number;
playlist?: string; playlist?: string;

View File

@ -4,10 +4,15 @@ import iconAdd from '/img/icon-add.svg';
import iconSubstract from '/img/icon-substract.svg'; import iconSubstract from '/img/icon-substract.svg';
import iconGridView from '/img/icon-gridview.svg'; import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg'; import iconListView from '/img/icon-listview.svg';
import { SortByType, SortOrderType } from '../pages/Home';
import { useUserConfigStore } from '../stores/UserConfigStore'; import { useUserConfigStore } from '../stores/UserConfigStore';
import { ViewStyles } from '../configuration/constants/ViewStyle'; import { ViewStyles } from '../configuration/constants/ViewStyle';
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig'; import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
import {
SortByEnum,
SortByType,
SortOrderEnum,
SortOrderType,
} from '../api/loader/loadVideoListByPage';
type FilterbarProps = { type FilterbarProps = {
hideToggleText: string; hideToggleText: string;
@ -67,12 +72,9 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
handleUserConfigUpdate({ sort_by: event.target.value as SortByType }); handleUserConfigUpdate({ sort_by: event.target.value as SortByType });
}} }}
> >
<option value="published">date published</option> {Object.entries(SortByEnum).map(([key, value]) => {
<option value="downloaded">date downloaded</option> return <option value={value}>{key}</option>;
<option value="views">views</option> })}
<option value="likes">likes</option>
<option value="duration">duration</option>
<option value="mediasize">media size</option>
</select> </select>
<select <select
name="sort_order" name="sort_order"
@ -82,8 +84,9 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
handleUserConfigUpdate({ sort_order: event.target.value as SortOrderType }); handleUserConfigUpdate({ sort_order: event.target.value as SortOrderType });
}} }}
> >
<option value="asc">asc</option> {Object.entries(SortOrderEnum).map(([key, value]) => {
<option value="desc">desc</option> return <option value={value}>{key}</option>;
})}
</select> </select>
</div> </div>
</div> </div>

View File

@ -26,6 +26,9 @@ const SettingsNavigation = () => {
<Link to={Routes.SettingsActions}> <Link to={Routes.SettingsActions}>
<h3>Actions</h3> <h3>Actions</h3>
</Link> </Link>
<Link to={Routes.SettingsVideos}>
<h3>Videos</h3>
</Link>
</> </>
)} )}
</div> </div>

View File

@ -0,0 +1,23 @@
import { SortOrderType } from '../api/loader/loadVideoListByPage';
const ARROW_UP = '↑';
const ARROW_DOWN = '↓';
type SortArrowProps = {
visible: boolean;
sortOrder: SortOrderType;
};
const SortArrow = ({ visible, sortOrder }: SortArrowProps) => {
if (!visible) {
return null;
}
if (sortOrder === 'asc') {
return ARROW_UP;
}
return ARROW_DOWN;
};
export default SortArrow;

View File

@ -17,6 +17,7 @@ const Routes = {
SettingsApplication: '/settings/application/', SettingsApplication: '/settings/application/',
SettingsScheduling: '/settings/scheduling/', SettingsScheduling: '/settings/scheduling/',
SettingsActions: '/settings/actions/', SettingsActions: '/settings/actions/',
SettingsVideos: '/settings/videos/',
Login: '/login/', Login: '/login/',
Video: (id: string) => `/video/${id}`, Video: (id: string) => `/video/${id}`,
VideoAtTimestamp: (id: string, timestamp: string) => `/video/${id}/?t=${timestamp}`, VideoAtTimestamp: (id: string, timestamp: string) => `/video/${id}/?t=${timestamp}`,

View File

@ -28,6 +28,7 @@ import Download from './pages/Download';
import loadUserAccount from './api/loader/loadUserAccount'; import loadUserAccount from './api/loader/loadUserAccount';
import loadAppsettingsConfig from './api/loader/loadAppsettingsConfig'; import loadAppsettingsConfig from './api/loader/loadAppsettingsConfig';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import SettingsVideos from './pages/SettingsVideos';
const router = createBrowserRouter( const router = createBrowserRouter(
[ [
@ -135,6 +136,10 @@ const router = createBrowserRouter(
path: Routes.SettingsUser, path: Routes.SettingsUser,
element: <SettingsUser />, element: <SettingsUser />,
}, },
{
path: Routes.SettingsVideos,
element: <SettingsVideos />,
},
{ {
path: Routes.About, path: Routes.About,
element: <About />, element: <About />,

View File

@ -99,8 +99,6 @@ export type ConfigType = {
downloads: DownloadsType; downloads: DownloadsType;
}; };
export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
export type SortOrderType = 'asc' | 'desc';
export type ViewLayoutType = 'grid' | 'list'; export type ViewLayoutType = 'grid' | 'list';
const Home = () => { const Home = () => {

View File

@ -0,0 +1,214 @@
import { useEffect, useState } from 'react';
import SettingsNavigation from '../components/SettingsNavigation';
import { ApiResponseType } from '../functions/APIClient';
import loadVideoListByFilter, {
SortByExpandedEnum,
SortByType,
SortOrderEnum,
SortOrderType,
VideoListByFilterResponseType,
WatchTypes,
WatchTypesEnum,
} from '../api/loader/loadVideoListByPage';
import { Link, useOutletContext } from 'react-router-dom';
import { OutletContextType } from './Base';
import Pagination from '../components/Pagination';
import humanFileSize from '../functions/humanFileSize';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { FileSizeUnits } from '../api/actions/updateUserConfig';
import Routes from '../configuration/routes/RouteList';
import SortArrow from '../components/SortArrow';
const SettingsVideos = () => {
const { userConfig } = useUserConfigStore();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const [refreshVideoList, setRefreshVideoList] = useState(false);
const [watchedState, setWatchedState] = useState<WatchTypes>('unwatched');
const [sortBy, setSortBy] = useState<SortByType>('mediasize');
const [sortOrder, setSortOrder] = useState<SortOrderType>('desc');
const [videoResponse, setVideoReponse] =
useState<ApiResponseType<VideoListByFilterResponseType>>();
const { data: videoResponseData } = videoResponse ?? {};
useEffect(() => {
(async () => {
const videos = await loadVideoListByFilter({
page: currentPage,
watch: watchedState,
sort: sortBy,
order: sortOrder,
});
setVideoReponse(videos);
setRefreshVideoList(false);
})();
}, [refreshVideoList, currentPage, watchedState, sortBy, sortOrder]);
const videoList = videoResponseData?.data;
const pagination = videoResponseData?.paginate;
const hasVideos = videoResponseData?.data?.length !== 0;
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
const toggleSortOrder = () => {
if (sortOrder === 'asc') {
setSortOrder('desc');
} else {
setSortOrder('asc');
}
};
const onHeaderClicked = (header: SortByType) => {
if (sortBy === header) {
toggleSortOrder();
} else {
setSortBy(header);
}
};
return (
<>
<title>TA | Videos</title>
<div className="boxed-content">
<SettingsNavigation />
<div className="title-bar">
<h1>Video details</h1>
</div>
<div className="settings-group video-details">
<p>
Show watched:{' '}
<select
id="id_watchedstate"
value={watchedState}
onChange={event => {
setWatchedState(event.currentTarget.value as WatchTypes);
}}
>
{Object.entries(WatchTypesEnum).map(([key, value]) => {
return (
<option key={key} value={value}>
{key}
</option>
);
})}
</select>
<br />
Sort by:
<select
name="sort_by"
id="sort"
value={sortBy}
onChange={event => {
setSortBy(event.currentTarget.value as SortByType);
}}
>
{Object.entries(SortByExpandedEnum).map(([key, value]) => {
return <option value={value}>{key}</option>;
})}
</select>
<select
name="sort_order"
id="sort-order"
value={sortOrder}
onChange={event => {
setSortOrder(event.currentTarget.value as SortOrderType);
}}
>
{Object.entries(SortOrderEnum).map(([key, value]) => {
return <option value={value}>{key}</option>;
})}
</select>
</p>
{!hasVideos && <p>No videos found</p>}
{hasVideos && (
<table>
<tbody>
{videoList?.map(({ youtube_id, title, channel, vid_type, media_size, streams }) => {
const [videoStream, audioStream] = streams;
return (
<tr key={youtube_id}>
<td>
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
</td>
<td>
<Link to={Routes.Video(youtube_id)}>{title}</Link>
</td>
<td>{vid_type}</td>
<td>{videoStream.width}</td>
<td>{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>
<thead>
<tr>
<th>Channel name</th>
<th>Video title</th>
<th>Type</th>
<th>
<a
onClick={() => {
onHeaderClicked('width');
}}
>
<div>
<SortArrow visible={sortBy === 'width'} sortOrder={sortOrder} />
Width
</div>
</a>
</th>
<th>
<a
onClick={() => {
onHeaderClicked('height');
}}
>
<SortArrow visible={sortBy === 'height'} sortOrder={sortOrder} />
Height
</a>
</th>
<th>
<a
onClick={() => {
onHeaderClicked('mediasize');
}}
>
<SortArrow visible={sortBy === 'mediasize'} sortOrder={sortOrder} />
Media size
</a>
</th>
<th>Video codec</th>
<th>Video bitrate</th>
<th>Audio codec</th>
<th>Audio bitrate</th>
</tr>
</thead>
</table>
)}
</div>
{pagination && (
<div className="boxed-content">
<Pagination pagination={pagination} setPage={setCurrentPage} />
</div>
)}
</div>
</>
);
};
export default SettingsVideos;

View File

@ -133,6 +133,11 @@ button:hover {
color: var(--main-bg); color: var(--main-bg);
} }
.video-details table td,
.video-details table th {
padding: 5px;
}
.button-box { .button-box {
padding: 5px 2px; padding: 5px 2px;
display: inline-flex; display: inline-flex;