mirror of
https://github.com/tubearchivist/tubearchivist.git
synced 2025-05-15 07:41:10 +00:00
Add Video details page to settings
This commit is contained in:
parent
1d727a1170
commit
776481c513
@ -31,6 +31,8 @@ class SortEnum(enum.Enum):
|
||||
LIKES = "stats.like_count"
|
||||
DURATION = "player.duration"
|
||||
MEDIASIZE = "media_size"
|
||||
WIDTH = "streams.width"
|
||||
HEIGHT = "streams.height"
|
||||
|
||||
@classmethod
|
||||
def values(cls) -> list[str]:
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SortByType, SortOrderType, ViewLayoutType } from '../../pages/Home';
|
||||
import { ViewLayoutType } from '../../pages/Home';
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { SortByType, SortOrderType } from '../loader/loadVideoListByPage';
|
||||
|
||||
export type ColourVariants =
|
||||
| 'dark.css'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
|
||||
import { ConfigType, VideoType } from '../../pages/Home';
|
||||
import { PaginationType } from '../../components/Pagination';
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
@ -8,9 +8,45 @@ export type VideoListByFilterResponseType = {
|
||||
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 WatchTypes = 'watched' | 'unwatched' | 'continue';
|
||||
export const WatchTypesEnum = {
|
||||
Watched: 'watched',
|
||||
Unwatched: 'unwatched',
|
||||
Continue: 'continue',
|
||||
};
|
||||
|
||||
type FilterType = {
|
||||
page?: number;
|
||||
playlist?: string;
|
||||
|
@ -4,10 +4,15 @@ 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 { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { ViewStyles } from '../configuration/constants/ViewStyle';
|
||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import {
|
||||
SortByEnum,
|
||||
SortByType,
|
||||
SortOrderEnum,
|
||||
SortOrderType,
|
||||
} from '../api/loader/loadVideoListByPage';
|
||||
|
||||
type FilterbarProps = {
|
||||
hideToggleText: string;
|
||||
@ -67,12 +72,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,8 +84,9 @@ 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>
|
||||
|
@ -26,6 +26,9 @@ const SettingsNavigation = () => {
|
||||
<Link to={Routes.SettingsActions}>
|
||||
<h3>Actions</h3>
|
||||
</Link>
|
||||
<Link to={Routes.SettingsVideos}>
|
||||
<h3>Videos</h3>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
23
frontend/src/components/SortArrow.tsx
Normal file
23
frontend/src/components/SortArrow.tsx
Normal 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;
|
@ -17,6 +17,7 @@ const Routes = {
|
||||
SettingsApplication: '/settings/application/',
|
||||
SettingsScheduling: '/settings/scheduling/',
|
||||
SettingsActions: '/settings/actions/',
|
||||
SettingsVideos: '/settings/videos/',
|
||||
Login: '/login/',
|
||||
Video: (id: string) => `/video/${id}`,
|
||||
VideoAtTimestamp: (id: string, timestamp: string) => `/video/${id}/?t=${timestamp}`,
|
||||
|
@ -28,6 +28,7 @@ import Download from './pages/Download';
|
||||
import loadUserAccount from './api/loader/loadUserAccount';
|
||||
import loadAppsettingsConfig from './api/loader/loadAppsettingsConfig';
|
||||
import NotFound from './pages/NotFound';
|
||||
import SettingsVideos from './pages/SettingsVideos';
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
@ -135,6 +136,10 @@ const router = createBrowserRouter(
|
||||
path: Routes.SettingsUser,
|
||||
element: <SettingsUser />,
|
||||
},
|
||||
{
|
||||
path: Routes.SettingsVideos,
|
||||
element: <SettingsVideos />,
|
||||
},
|
||||
{
|
||||
path: Routes.About,
|
||||
element: <About />,
|
||||
|
@ -99,8 +99,6 @@ export type ConfigType = {
|
||||
downloads: DownloadsType;
|
||||
};
|
||||
|
||||
export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
|
||||
export type SortOrderType = 'asc' | 'desc';
|
||||
export type ViewLayoutType = 'grid' | 'list';
|
||||
|
||||
const Home = () => {
|
||||
|
214
frontend/src/pages/SettingsVideos.tsx
Normal file
214
frontend/src/pages/SettingsVideos.tsx
Normal 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;
|
@ -133,6 +133,11 @@ button:hover {
|
||||
color: var(--main-bg);
|
||||
}
|
||||
|
||||
.video-details table td,
|
||||
.video-details table th {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.button-box {
|
||||
padding: 5px 2px;
|
||||
display: inline-flex;
|
||||
|
Loading…
x
Reference in New Issue
Block a user