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"
|
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]:
|
||||||
|
@ -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'
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
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/',
|
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}`,
|
||||||
|
@ -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 />,
|
||||||
|
@ -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 = () => {
|
||||||
|
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);
|
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user