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:
parent
75339e479e
commit
5a5d47da9b
@ -13,7 +13,6 @@ from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from common.src.env_settings import EnvironmentSettings
|
||||
from common.src.es_connect import IndexPaginate
|
||||
|
||||
|
||||
@ -215,15 +214,8 @@ def ta_host_parser(ta_host: str) -> tuple[list[str], list[str]]:
|
||||
|
||||
def get_stylesheets() -> list:
|
||||
"""Get all valid stylesheets from /static/css"""
|
||||
app_root = EnvironmentSettings.APP_DIR
|
||||
try:
|
||||
stylesheets = os.listdir(os.path.join(app_root, "static/css"))
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
stylesheets.remove("style.css")
|
||||
stylesheets.sort()
|
||||
stylesheets = list(filter(lambda x: x.endswith(".css"), stylesheets))
|
||||
stylesheets = ["dark.css", "light.css", "matrix.css", "midnight.css"]
|
||||
return stylesheets
|
||||
|
||||
|
||||
|
1722
frontend/package-lock.json
generated
1722
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,26 +10,23 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.1.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-router-dom": "^6.25.1"
|
||||
"dompurify": "^3.2.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.8",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4"
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
36
frontend/src/api/actions/createAppriseNotificationUrl.ts
Normal file
36
frontend/src/api/actions/createAppriseNotificationUrl.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import getCookie from '../../functions/getCookie';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
|
||||
export type AppriseTaskNameType =
|
||||
| 'update_subscribed'
|
||||
| 'extract_download'
|
||||
| 'download_pending'
|
||||
| 'check_reindex';
|
||||
|
||||
const createAppriseNotificationUrl = async (taskName: AppriseTaskNameType, url: string) => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfCookie = getCookie('csrftoken');
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/task/notification/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-CSRFToken': csrfCookie || '',
|
||||
},
|
||||
credentials: getFetchCredentials(),
|
||||
body: JSON.stringify({ task_name: taskName, url }),
|
||||
});
|
||||
|
||||
const appriseNotificationUrl = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('createAppriseNotificationUrl', appriseNotificationUrl);
|
||||
}
|
||||
|
||||
return appriseNotificationUrl;
|
||||
};
|
||||
|
||||
export default createAppriseNotificationUrl;
|
53
frontend/src/api/actions/createTaskSchedule.ts
Normal file
53
frontend/src/api/actions/createTaskSchedule.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import getCookie from '../../functions/getCookie';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
|
||||
export type TaskScheduleNameType =
|
||||
| 'update_subscribed'
|
||||
| 'download_pending'
|
||||
| 'extract_download'
|
||||
| 'check_reindex'
|
||||
| 'manual_import'
|
||||
| 'run_backup'
|
||||
| 'restore_backup'
|
||||
| 'rescan_filesystem'
|
||||
| 'thumbnail_check'
|
||||
| 'resync_thumbs'
|
||||
| 'index_playlists'
|
||||
| 'subscribe_to'
|
||||
| 'version_check';
|
||||
|
||||
type ScheduleConfigType = {
|
||||
schedule?: string;
|
||||
config?: {
|
||||
days?: number;
|
||||
rotate?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const createTaskSchedule = async (taskName: TaskScheduleNameType, schedule: ScheduleConfigType) => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfCookie = getCookie('csrftoken');
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/task/schedule/${taskName}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-CSRFToken': csrfCookie || '',
|
||||
},
|
||||
credentials: getFetchCredentials(),
|
||||
body: JSON.stringify(schedule),
|
||||
});
|
||||
|
||||
const scheduledTask = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('createTaskSchedule', scheduledTask);
|
||||
}
|
||||
|
||||
return scheduledTask;
|
||||
};
|
||||
|
||||
export default createTaskSchedule;
|
36
frontend/src/api/actions/deleteAppriseNotificationUrl.ts
Normal file
36
frontend/src/api/actions/deleteAppriseNotificationUrl.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import getCookie from '../../functions/getCookie';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
|
||||
type AppriseTaskNameType =
|
||||
| 'update_subscribed'
|
||||
| 'extract_download'
|
||||
| 'download_pending'
|
||||
| 'check_reindex';
|
||||
|
||||
const deleteAppriseNotificationUrl = async (taskName: AppriseTaskNameType) => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfCookie = getCookie('csrftoken');
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/task/notification/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-CSRFToken': csrfCookie || '',
|
||||
},
|
||||
credentials: getFetchCredentials(),
|
||||
body: JSON.stringify({ task_name: taskName }),
|
||||
});
|
||||
|
||||
const appriseNotification = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('deleteAppriseNotificationUrl', appriseNotification);
|
||||
}
|
||||
|
||||
return appriseNotification;
|
||||
};
|
||||
|
||||
export default deleteAppriseNotificationUrl;
|
30
frontend/src/api/actions/deleteTaskSchedule.ts
Normal file
30
frontend/src/api/actions/deleteTaskSchedule.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import getCookie from '../../functions/getCookie';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
import { TaskScheduleNameType } from './createTaskSchedule';
|
||||
|
||||
const deleteTaskSchedule = async (taskName: TaskScheduleNameType) => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfCookie = getCookie('csrftoken');
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/task/schedule/${taskName}/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-CSRFToken': csrfCookie || '',
|
||||
},
|
||||
credentials: getFetchCredentials(),
|
||||
});
|
||||
|
||||
const scheduledTask = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('deleteTaskSchedule', scheduledTask);
|
||||
}
|
||||
|
||||
return scheduledTask;
|
||||
};
|
||||
|
||||
export default deleteTaskSchedule;
|
39
frontend/src/api/actions/updateChannelSettings.ts
Normal file
39
frontend/src/api/actions/updateChannelSettings.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import getCookie from '../../functions/getCookie';
|
||||
|
||||
export type ChannelAboutConfigType = {
|
||||
index_playlists?: boolean;
|
||||
download_format?: boolean | string;
|
||||
autodelete_days?: boolean | number;
|
||||
integrate_sponsorblock?: boolean | null;
|
||||
subscriptions_channel_size?: number;
|
||||
subscriptions_live_channel_size?: number;
|
||||
subscriptions_shorts_channel_size?: number;
|
||||
};
|
||||
|
||||
const updateChannelSettings = async (channelId: string, config: ChannelAboutConfigType) => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfCookie = getCookie('csrftoken');
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/channel/${channelId}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-CSRFToken': csrfCookie || '',
|
||||
},
|
||||
credentials: getFetchCredentials(),
|
||||
|
||||
body: JSON.stringify({
|
||||
channel_overwrites: config,
|
||||
}),
|
||||
});
|
||||
|
||||
const channelSubscription = await response.json();
|
||||
console.log('updateChannelSettings', channelSubscription);
|
||||
|
||||
return channelSubscription;
|
||||
};
|
||||
|
||||
export default updateChannelSettings;
|
@ -8,6 +8,7 @@ export type ValidatedCookieType = {
|
||||
status: boolean;
|
||||
validated: number;
|
||||
validated_str: string;
|
||||
cookie_validated?: boolean;
|
||||
};
|
||||
|
||||
const updateCookie = async (): Promise<ValidatedCookieType> => {
|
||||
|
@ -12,6 +12,7 @@ const loadApiToken = async (): Promise<ApiTokenResponse> => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfCookie = getCookie('csrftoken');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
@ -27,6 +28,9 @@ const loadApiToken = async (): Promise<ApiTokenResponse> => {
|
||||
}
|
||||
|
||||
return apiToken;
|
||||
} catch (e) {
|
||||
return { token: '' };
|
||||
}
|
||||
};
|
||||
|
||||
export default loadApiToken;
|
||||
|
42
frontend/src/api/loader/loadAppriseNotification.ts
Normal file
42
frontend/src/api/loader/loadAppriseNotification.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
|
||||
export type AppriseNotificationType = {
|
||||
check_reindex?: {
|
||||
urls: string[];
|
||||
title: string;
|
||||
};
|
||||
download_pending?: {
|
||||
urls: string[];
|
||||
title: string;
|
||||
};
|
||||
extract_download?: {
|
||||
urls: string[];
|
||||
title: string;
|
||||
};
|
||||
update_subscribed?: {
|
||||
urls: string[];
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
const loadAppriseNotification = async (): Promise<AppriseNotificationType> => {
|
||||
const apiUrl = getApiUrl();
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/task/notification/`, {
|
||||
headers: defaultHeaders,
|
||||
credentials: getFetchCredentials(),
|
||||
});
|
||||
|
||||
const notification = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('loadAppriseNotification', notification);
|
||||
}
|
||||
|
||||
return notification;
|
||||
};
|
||||
|
||||
export default loadAppriseNotification;
|
@ -11,20 +11,20 @@ export type AppSettingsConfigType = {
|
||||
auto_start: boolean;
|
||||
};
|
||||
downloads: {
|
||||
limit_speed: boolean | number;
|
||||
limit_speed: false | number;
|
||||
sleep_interval: number;
|
||||
autodelete_days: boolean | number;
|
||||
format: boolean | string;
|
||||
autodelete_days: number;
|
||||
format: number | string;
|
||||
format_sort: boolean | string;
|
||||
add_metadata: boolean;
|
||||
add_thumbnail: boolean;
|
||||
subtitle: boolean | string;
|
||||
subtitle_source: boolean | string;
|
||||
subtitle_index: boolean;
|
||||
comment_max: boolean | number;
|
||||
comment_max: string | number;
|
||||
comment_sort: string;
|
||||
cookie_import: boolean;
|
||||
throttledratelimit: boolean | number;
|
||||
throttledratelimit: false | number;
|
||||
extractor_lang: boolean | string;
|
||||
integrate_ryd: boolean;
|
||||
integrate_sponsorblock: boolean;
|
||||
|
36
frontend/src/api/loader/loadChannelAggs.ts
Normal file
36
frontend/src/api/loader/loadChannelAggs.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
|
||||
export type ChannelAggsType = {
|
||||
total_items: {
|
||||
value: number;
|
||||
};
|
||||
total_size: {
|
||||
value: number;
|
||||
};
|
||||
total_duration: {
|
||||
value: number;
|
||||
value_str: string;
|
||||
};
|
||||
};
|
||||
|
||||
const loadChannelAggs = async (channelId: string): Promise<ChannelAggsType> => {
|
||||
const apiUrl = getApiUrl();
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/channel/${channelId}/aggs/`, {
|
||||
headers: defaultHeaders,
|
||||
credentials: getFetchCredentials(),
|
||||
});
|
||||
|
||||
const channel = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('loadChannelAggs', channel);
|
||||
}
|
||||
|
||||
return channel;
|
||||
};
|
||||
|
||||
export default loadChannelAggs;
|
36
frontend/src/api/loader/loadSchedule.ts
Normal file
36
frontend/src/api/loader/loadSchedule.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||
import getApiUrl from '../../configuration/getApiUrl';
|
||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||
|
||||
type ScheduleType = {
|
||||
name: string;
|
||||
schedule: string;
|
||||
schedule_human: string;
|
||||
last_run_at: string;
|
||||
config: {
|
||||
days?: number;
|
||||
rotate?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ScheduleResponseType = ScheduleType[];
|
||||
|
||||
const loadSchedule = async (): Promise<ScheduleResponseType> => {
|
||||
const apiUrl = getApiUrl();
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/task/schedule/`, {
|
||||
headers: defaultHeaders,
|
||||
credentials: getFetchCredentials(),
|
||||
});
|
||||
|
||||
const schedule = await response.json();
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
console.log('loadSchedule', schedule);
|
||||
}
|
||||
|
||||
return schedule;
|
||||
};
|
||||
|
||||
export default loadSchedule;
|
@ -12,7 +12,7 @@ export type VideoListByFilterResponseType = {
|
||||
};
|
||||
|
||||
type WatchTypes = 'watched' | 'unwatched' | 'continue';
|
||||
type VideoTypes = 'videos' | 'streams' | 'shorts';
|
||||
export type VideoTypes = 'videos' | 'streams' | 'shorts';
|
||||
|
||||
type FilterType = {
|
||||
page?: number;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface ButtonProps {
|
||||
id?: string;
|
||||
name?: string;
|
||||
className?: string;
|
||||
type?: 'submit' | 'reset' | 'button' | undefined;
|
||||
label?: string | JSX.Element | JSX.Element[];
|
||||
children?: string | JSX.Element | JSX.Element[];
|
||||
label?: string | ReactNode | ReactNode[];
|
||||
children?: string | ReactNode | ReactNode[];
|
||||
value?: string;
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
|
@ -2,14 +2,15 @@ import getApiUrl from '../configuration/getApiUrl';
|
||||
import defaultChannelImage from '/img/default-channel-banner.jpg';
|
||||
|
||||
type ChannelIconProps = {
|
||||
channel_id: string;
|
||||
channelId: string;
|
||||
channelBannerUrl: string | undefined;
|
||||
};
|
||||
|
||||
const ChannelBanner = ({ channel_id }: ChannelIconProps) => {
|
||||
const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
|
||||
return (
|
||||
<img
|
||||
src={`${getApiUrl()}/cache/channels/${channel_id}_banner.jpg`}
|
||||
alt={`${channel_id}-banner`}
|
||||
src={`${getApiUrl()}${channelBannerUrl}`}
|
||||
alt={`${channelId}-banner`}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
currentTarget.src = defaultChannelImage;
|
||||
|
@ -2,14 +2,15 @@ import getApiUrl from '../configuration/getApiUrl';
|
||||
import defaultChannelIcon from '/img/default-channel-icon.jpg';
|
||||
|
||||
type ChannelIconProps = {
|
||||
channel_id: string;
|
||||
channelId: string;
|
||||
channelThumbUrl: string | undefined;
|
||||
};
|
||||
|
||||
const ChannelIcon = ({ channel_id }: ChannelIconProps) => {
|
||||
const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
|
||||
return (
|
||||
<img
|
||||
src={`${getApiUrl()}/cache/channels/${channel_id}_thumb.jpg`}
|
||||
alt="channel-thumb"
|
||||
src={`${getApiUrl()}${channelThumbUrl}`}
|
||||
alt={`${channelId}-thumb`}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
currentTarget.src = defaultChannelIcon;
|
||||
|
@ -27,14 +27,20 @@ const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelLis
|
||||
<div key={channel.channel_id} className={`channel-item ${viewLayout}`}>
|
||||
<div className={`channel-banner ${viewLayout}`}>
|
||||
<Link to={Routes.Channel(channel.channel_id)}>
|
||||
<ChannelBanner channel_id={channel.channel_id} />
|
||||
<ChannelBanner
|
||||
channelId={channel.channel_id}
|
||||
channelBannerUrl={channel.channel_banner_url}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`info-box info-box-2 ${viewLayout}`}>
|
||||
<div className="info-box-item">
|
||||
<div className="round-img">
|
||||
<Link to={Routes.Channel(channel.channel_id)}>
|
||||
<ChannelIcon channel_id={channel.channel_id} />
|
||||
<ChannelIcon
|
||||
channelId={channel.channel_id}
|
||||
channelThumbUrl={channel.channel_thumb_url}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
@ -59,6 +65,7 @@ const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelLis
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!channel.channel_subscribed && (
|
||||
<Button
|
||||
label="Subscribe"
|
||||
@ -66,7 +73,10 @@ const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelLis
|
||||
title={`Subscribe to ${channel.channel_name}`}
|
||||
onClick={async () => {
|
||||
await updateChannelSubscription(channel.channel_id, true);
|
||||
|
||||
setTimeout(() => {
|
||||
refreshChannelList(true);
|
||||
}, 500);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -10,6 +10,7 @@ type ChannelOverviewProps = {
|
||||
channelname: string;
|
||||
channelSubs: number;
|
||||
channelSubscribed: boolean;
|
||||
channelThumbUrl: string;
|
||||
showSubscribeButton?: boolean;
|
||||
isUserAdmin?: boolean;
|
||||
setRefresh: (status: boolean) => void;
|
||||
@ -20,6 +21,7 @@ const ChannelOverview = ({
|
||||
channelSubs,
|
||||
channelSubscribed,
|
||||
channelname,
|
||||
channelThumbUrl,
|
||||
showSubscribeButton = false,
|
||||
isUserAdmin,
|
||||
setRefresh,
|
||||
@ -29,7 +31,7 @@ const ChannelOverview = ({
|
||||
<div className="info-box-item">
|
||||
<div className="round-img">
|
||||
<Link to={Routes.Channel(channelId)}>
|
||||
<ChannelIcon channel_id={channelId} />
|
||||
<ChannelIcon channelId={channelId} channelThumbUrl={channelThumbUrl} />
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -115,7 +115,8 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
|
||||
id: videoId,
|
||||
is_watched: status,
|
||||
});
|
||||
|
||||
}}
|
||||
onDone={() => {
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
|
@ -48,13 +48,6 @@ const Filterbar = ({
|
||||
}: FilterbarProps) => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (
|
||||
userMeConfig.hide_watched !== hideWatched ||
|
||||
userMeConfig[viewStyleName.toString() as keyof typeof userMeConfig] !== view ||
|
||||
userMeConfig.grid_items !== gridItems ||
|
||||
userMeConfig.sort_by !== sortBy ||
|
||||
userMeConfig.sort_order !== sortOrder
|
||||
) {
|
||||
const userConfig: UserConfigType = {
|
||||
hide_watched: hideWatched,
|
||||
[viewStyleName.toString()]: view,
|
||||
@ -65,7 +58,6 @@ const Filterbar = ({
|
||||
|
||||
await updateUserConfig(userConfig);
|
||||
setRefresh?.(true);
|
||||
}
|
||||
})();
|
||||
}, [hideWatched, view, gridItems, sortBy, sortOrder, viewStyleName, setRefresh, userMeConfig]);
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { VideoType } from '../pages/Home';
|
||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
|
||||
@ -214,12 +213,11 @@ const GoogleCast = ({ video, videoProgress, setRefresh }: GoogleCastProps) => {
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<Helmet>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||
></script>
|
||||
</Helmet>
|
||||
|
||||
{/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
|
||||
<google-cast-launcher id="castbutton"></google-cast-launcher>
|
||||
</>
|
||||
|
@ -1,7 +1,7 @@
|
||||
const PaginationDummy = () => {
|
||||
return (
|
||||
<div className="boxed-content">
|
||||
<div className="pagination">{/** dummy pagination for padding */}</div>
|
||||
<div className="pagination">{/** dummy pagination for consistent padding */}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { PlaylistType } from '../pages/Playlist';
|
||||
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
||||
import formatDate from '../functions/formatDates';
|
||||
import Button from './Button';
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
import PlaylistThumbnail from './PlaylistThumbnail';
|
||||
|
||||
type PlaylistListProps = {
|
||||
playlistList: PlaylistType[] | undefined;
|
||||
@ -25,9 +25,9 @@ const PlaylistList = ({ playlistList, viewLayout, setRefresh }: PlaylistListProp
|
||||
<div key={playlist.playlist_id} className={`playlist-item ${viewLayout}`}>
|
||||
<div className="playlist-thumbnail">
|
||||
<Link to={Routes.Playlist(playlist.playlist_id)}>
|
||||
<img
|
||||
src={`${getApiUrl()}/cache/playlists/${playlist.playlist_id}.jpg`}
|
||||
alt={`${playlist.playlist_id}-thumbnail`}
|
||||
<PlaylistThumbnail
|
||||
playlistId={playlist.playlist_id}
|
||||
playlistThumbnail={playlist.playlist_thumbnail}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
@ -68,7 +68,9 @@ const PlaylistList = ({ playlistList, viewLayout, setRefresh }: PlaylistListProp
|
||||
onClick={async () => {
|
||||
await updatePlaylistSubscription(playlist.playlist_id, true);
|
||||
|
||||
setTimeout(() => {
|
||||
setRefresh(true);
|
||||
}, 500);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
22
frontend/src/components/PlaylistThumbnail.tsx
Normal file
22
frontend/src/components/PlaylistThumbnail.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
import defaultPlaylistThumbnail from '/img/default-playlist-thumb.jpg';
|
||||
|
||||
type PlaylistThumbnailProps = {
|
||||
playlistId: string;
|
||||
playlistThumbnail: string | undefined;
|
||||
};
|
||||
|
||||
const PlaylistThumbnail = ({ playlistId, playlistThumbnail }: PlaylistThumbnailProps) => {
|
||||
return (
|
||||
<img
|
||||
src={`${getApiUrl()}${playlistThumbnail}`}
|
||||
alt={`${playlistId}-thumbnail`}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
currentTarget.src = defaultPlaylistThumbnail;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistThumbnail;
|
@ -79,7 +79,8 @@ const VideoListItem = ({
|
||||
id: video.youtube_id,
|
||||
is_watched: status,
|
||||
});
|
||||
|
||||
}}
|
||||
onDone={() => {
|
||||
refreshVideoList(true);
|
||||
}}
|
||||
/>
|
||||
|
@ -99,6 +99,47 @@ const handleTimeUpdate =
|
||||
}
|
||||
};
|
||||
|
||||
export type VideoProgressType = {
|
||||
youtube_id: string;
|
||||
user_id: number;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type VideoPlayerProps = {
|
||||
video: VideoResponseType;
|
||||
videoProgress?: VideoProgressType;
|
||||
sponsorBlock?: SponsorBlockType;
|
||||
embed?: boolean;
|
||||
autoplay?: boolean;
|
||||
onVideoEnd?: () => void;
|
||||
};
|
||||
|
||||
const VideoPlayer = ({
|
||||
video,
|
||||
videoProgress,
|
||||
sponsorBlock,
|
||||
embed,
|
||||
autoplay = false,
|
||||
onVideoEnd,
|
||||
}: VideoPlayerProps) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const searchParamVideoProgress = searchParams.get('t');
|
||||
|
||||
const [skippedSegments, setSkippedSegments] = useState<SponsorSegmentsSkippedType>({});
|
||||
|
||||
const videoId = video.data.youtube_id;
|
||||
const videoUrl = video.data.media_url;
|
||||
const videoThumbUrl = video.data.vid_thumb_url;
|
||||
const watched = video.data.player.watched;
|
||||
const duration = video.data.player.duration;
|
||||
const videoSubtitles = video.data.subtitles;
|
||||
|
||||
let videoSrcProgress = Number(videoProgress?.position) > 0 ? Number(videoProgress?.position) : '';
|
||||
|
||||
if (searchParamVideoProgress !== null) {
|
||||
videoSrcProgress = searchParamVideoProgress;
|
||||
}
|
||||
|
||||
const handleVideoEnd =
|
||||
(
|
||||
youtubeId: string,
|
||||
@ -120,42 +161,10 @@ const handleVideoEnd =
|
||||
|
||||
return segments;
|
||||
});
|
||||
|
||||
onVideoEnd?.();
|
||||
};
|
||||
|
||||
export type VideoProgressType = {
|
||||
youtube_id: string;
|
||||
user_id: number;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type VideoPlayerProps = {
|
||||
video: VideoResponseType;
|
||||
videoProgress?: VideoProgressType;
|
||||
sponsorBlock?: SponsorBlockType;
|
||||
embed?: boolean;
|
||||
};
|
||||
|
||||
const VideoPlayer = ({ video, videoProgress, sponsorBlock, embed }: VideoPlayerProps) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const searchParamVideoProgress = searchParams.get('t');
|
||||
|
||||
const [skippedSegments, setSkippedSegments] = useState<SponsorSegmentsSkippedType>({});
|
||||
|
||||
const videoId = video.data.youtube_id;
|
||||
const videoUrl = video.data.media_url;
|
||||
const videoThumbUrl = video.data.vid_thumb_url;
|
||||
const watched = video.data.player.watched;
|
||||
const duration = video.data.player.duration;
|
||||
const videoSubtitles = video.data.subtitles;
|
||||
|
||||
let videoSrcProgress = Number(videoProgress?.position) > 0 ? Number(videoProgress?.position) : '';
|
||||
|
||||
if (searchParamVideoProgress !== null) {
|
||||
videoSrcProgress = searchParamVideoProgress;
|
||||
}
|
||||
|
||||
const autoplay = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="player" className={embed ? '' : 'player-wrapper'}>
|
||||
@ -165,8 +174,13 @@ const VideoPlayer = ({ video, videoProgress, sponsorBlock, embed }: VideoPlayerP
|
||||
onVolumeChange={(videoTag: VideoTag) => {
|
||||
localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
|
||||
}}
|
||||
onRateChange={(videoTag: VideoTag) => {
|
||||
localStorage.setItem('playerSpeed', videoTag.currentTarget.playbackRate.toString());
|
||||
}}
|
||||
onLoadStart={(videoTag: VideoTag) => {
|
||||
videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume')) ?? 1;
|
||||
videoTag.currentTarget.playbackRate =
|
||||
Number(localStorage.getItem('playerSpeed')) ?? 1;
|
||||
}}
|
||||
onTimeUpdate={handleTimeUpdate(
|
||||
videoId,
|
||||
|
@ -1,33 +1,63 @@
|
||||
import iconUnseen from '/img/icon-unseen.svg';
|
||||
import iconSeen from '/img/icon-seen.svg';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type WatchedCheckBoxProps = {
|
||||
watched: boolean;
|
||||
onClick?: (status: boolean) => void;
|
||||
onDone?: (status: boolean) => void;
|
||||
};
|
||||
|
||||
const WatchedCheckBox = ({ watched, onClick }: WatchedCheckBoxProps) => {
|
||||
const WatchedCheckBox = ({ watched, onClick, onDone }: WatchedCheckBoxProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [state, setState] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
onClick?.(state);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
onDone?.(state);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{watched && (
|
||||
{loading && (
|
||||
<>
|
||||
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
|
||||
<div />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!loading && watched && (
|
||||
<img
|
||||
src={iconSeen}
|
||||
alt="seen-icon"
|
||||
className="watch-button"
|
||||
title="Mark as unwatched"
|
||||
onClick={async () => {
|
||||
onClick?.(false);
|
||||
setState(false);
|
||||
setLoading(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!watched && (
|
||||
{!loading && !watched && (
|
||||
<img
|
||||
src={iconUnseen}
|
||||
alt="unseen-icon"
|
||||
className="watch-button"
|
||||
title="Mark as watched"
|
||||
onClick={async () => {
|
||||
onClick?.(true);
|
||||
setState(true);
|
||||
setLoading(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -25,9 +25,7 @@ import ChannelBase from './pages/ChannelBase';
|
||||
import ChannelVideo from './pages/ChannelVideo';
|
||||
import ChannelPlaylist from './pages/ChannelPlaylist';
|
||||
import ChannelAbout from './pages/ChannelAbout';
|
||||
import ChannelStream from './pages/ChannelStream';
|
||||
import Download from './pages/Download';
|
||||
import ChannelShorts from './pages/ChannelShorts';
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
@ -105,7 +103,7 @@ const router = createBrowserRouter(
|
||||
{
|
||||
index: true,
|
||||
path: Routes.ChannelVideo(':channelId'),
|
||||
element: <ChannelVideo />,
|
||||
element: <ChannelVideo videoType="videos" />,
|
||||
loader: async () => {
|
||||
const authResponse = await loadAuth();
|
||||
if (authResponse.status === 403) {
|
||||
@ -119,26 +117,30 @@ const router = createBrowserRouter(
|
||||
},
|
||||
{
|
||||
path: Routes.ChannelStream(':channelId'),
|
||||
element: <ChannelStream />,
|
||||
element: <ChannelVideo videoType="streams" />,
|
||||
loader: async () => {
|
||||
const authResponse = await loadAuth();
|
||||
if (authResponse.status === 403) {
|
||||
return redirect(Routes.Login);
|
||||
}
|
||||
|
||||
return {};
|
||||
const userConfig = await loadUserMeConfig();
|
||||
|
||||
return { userConfig };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: Routes.ChannelShorts(':channelId'),
|
||||
element: <ChannelShorts />,
|
||||
element: <ChannelVideo videoType="shorts" />,
|
||||
loader: async () => {
|
||||
const authResponse = await loadAuth();
|
||||
if (authResponse.status === 403) {
|
||||
return redirect(Routes.Login);
|
||||
}
|
||||
|
||||
return {};
|
||||
const userConfig = await loadUserMeConfig();
|
||||
|
||||
return { userConfig };
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | About</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="boxed-content">
|
||||
<div className="title-bar">
|
||||
|
@ -10,18 +10,16 @@ import queueReindex, { ReindexType, ReindexTypeEnum } from '../api/actions/queue
|
||||
import formatDate from '../functions/formatDates';
|
||||
import PaginationDummy from '../components/PaginationDummy';
|
||||
import FormattedNumber from '../components/FormattedNumber';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import updateChannelSettings, {
|
||||
ChannelAboutConfigType,
|
||||
} from '../api/actions/updateChannelSettings';
|
||||
|
||||
const handleSponsorBlockIntegrationOverwrite = (integration: boolean | undefined) => {
|
||||
if (integration === undefined) {
|
||||
return 'False';
|
||||
}
|
||||
|
||||
if (integration) {
|
||||
return integration;
|
||||
} else {
|
||||
return 'Disabled';
|
||||
const toStringToBool = (str: string) => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@ -51,9 +49,26 @@ const ChannelAbout = () => {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
const [reindex, setReindex] = useState(false);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||
const [channelConfig, setChannelConfig] = useState<ChannelAboutConfigType>();
|
||||
|
||||
const [downloadFormat, setDownloadFormat] = useState(channelConfig?.download_format);
|
||||
const [autoDeleteAfter, setAutoDeleteAfter] = useState(channelConfig?.autodelete_days);
|
||||
const [indexPlaylists, setIndexPlaylists] = useState(
|
||||
channelConfig?.index_playlists ? 'true' : 'false',
|
||||
);
|
||||
const [enableSponsorblock, setEnableSponsorblock] = useState(
|
||||
channelConfig?.integrate_sponsorblock,
|
||||
);
|
||||
const [pageSizeVideo, setPageSizeVideo] = useState(channelConfig?.subscriptions_channel_size);
|
||||
const [pageSizeStreams, setPageSizeStreams] = useState(
|
||||
channelConfig?.subscriptions_live_channel_size,
|
||||
);
|
||||
const [pageSizeShorts, setPageSizeShorts] = useState(
|
||||
channelConfig?.subscriptions_shorts_channel_size,
|
||||
);
|
||||
|
||||
const channel = channelResponse?.data;
|
||||
|
||||
@ -62,16 +77,31 @@ const ChannelAbout = () => {
|
||||
const handleSubmit = async (event: { preventDefault: () => void }) => {
|
||||
event.preventDefault();
|
||||
|
||||
//TODO: implement request to about api endpoint ( when implemented )
|
||||
// `/api/channel/${channel.channel_id}/about/`
|
||||
await updateChannelSettings(channelId, {
|
||||
index_playlists: toStringToBool(indexPlaylists),
|
||||
download_format: downloadFormat,
|
||||
autodelete_days: autoDeleteAfter,
|
||||
integrate_sponsorblock: enableSponsorblock,
|
||||
subscriptions_channel_size: pageSizeVideo,
|
||||
subscriptions_live_channel_size: pageSizeStreams,
|
||||
subscriptions_shorts_channel_size: pageSizeShorts,
|
||||
});
|
||||
|
||||
setRefresh(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (refresh) {
|
||||
const channelResponse = await loadChannelById(channelId);
|
||||
|
||||
setChannelResponse(channelResponse);
|
||||
setChannelConfig(channelResponse?.data?.channel_overwrites);
|
||||
console.log('channel_overwrites', channelResponse?.data);
|
||||
console.log('channel_overwrites', channelResponse?.data?.channel_overwrites);
|
||||
console.log('channel_overwrites', '--------');
|
||||
setRefresh(false);
|
||||
}
|
||||
})();
|
||||
}, [refresh, channelId]);
|
||||
|
||||
@ -81,9 +111,7 @@ const ChannelAbout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Channel: About {channel.channel_name}</title>
|
||||
</Helmet>
|
||||
<title>{`TA | Channel: About ${channel.channel_name}`}</title>
|
||||
<div className="boxed-content">
|
||||
<div className="info-box info-box-3">
|
||||
<ChannelOverview
|
||||
@ -91,6 +119,7 @@ const ChannelAbout = () => {
|
||||
channelname={channel.channel_name}
|
||||
channelSubs={channel.channel_subs}
|
||||
channelSubscribed={channel.channel_subscribed}
|
||||
channelThumbUrl={channel.channel_thumb_url}
|
||||
showSubscribeButton={true}
|
||||
isUserAdmin={isAdmin}
|
||||
setRefresh={setRefresh}
|
||||
@ -224,7 +253,22 @@ const ChannelAbout = () => {
|
||||
{channelOverwrites?.download_format || 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<input type="text" name="download_format" id="id_download_format" />
|
||||
<input
|
||||
type="text"
|
||||
name="download_format"
|
||||
id="id_download_format"
|
||||
onChange={event => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
setDownloadFormat(value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Reset"
|
||||
onClick={() => {
|
||||
setDownloadFormat(false);
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<div className="overwrite-form-item">
|
||||
@ -234,7 +278,20 @@ const ChannelAbout = () => {
|
||||
{channelOverwrites?.autodelete_days || 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<input type="number" name="autodelete_days" id="id_autodelete_days" />
|
||||
<input
|
||||
type="number"
|
||||
name="autodelete_days"
|
||||
id="id_autodelete_days"
|
||||
onChange={event => {
|
||||
const value = Number(event.currentTarget.value);
|
||||
|
||||
if (value === 0) {
|
||||
setAutoDeleteAfter(false);
|
||||
} else {
|
||||
setAutoDeleteAfter(Number(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<br />
|
||||
</div>
|
||||
@ -243,12 +300,21 @@ const ChannelAbout = () => {
|
||||
<p>
|
||||
Index playlists:{' '}
|
||||
<span className="settings-current">
|
||||
{channelOverwrites?.index_playlists || 'False'}
|
||||
{JSON.stringify(channelOverwrites?.index_playlists)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<select name="index_playlists" id="id_index_playlists" defaultValue="">
|
||||
<option value="">-- change playlist index --</option>
|
||||
<select
|
||||
name="index_playlists"
|
||||
id="id_index_playlists"
|
||||
value={indexPlaylists}
|
||||
onChange={event => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
setIndexPlaylists(value);
|
||||
}}
|
||||
>
|
||||
<option value="null">-- change playlist index --</option>
|
||||
<option value="false">Disable playlist index</option>
|
||||
<option value="true">Enable playlist index</option>
|
||||
</select>
|
||||
@ -264,20 +330,27 @@ const ChannelAbout = () => {
|
||||
</a>
|
||||
:{' '}
|
||||
<span className="settings-current">
|
||||
{handleSponsorBlockIntegrationOverwrite(
|
||||
channelOverwrites?.integrate_sponsorblock,
|
||||
)}
|
||||
{JSON.stringify(channelOverwrites?.integrate_sponsorblock)}
|
||||
</span>
|
||||
</p>
|
||||
<select
|
||||
name="integrate_sponsorblock"
|
||||
id="id_integrate_sponsorblock"
|
||||
defaultValue=""
|
||||
value={enableSponsorblock?.toString() || ''}
|
||||
onChange={event => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
if (value !== '') {
|
||||
setEnableSponsorblock(JSON.parse(value));
|
||||
} else {
|
||||
setEnableSponsorblock(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">-- change sponsorblock integrations</option>
|
||||
<option value="disable">disable sponsorblock integration</option>
|
||||
<option value="false">disable sponsorblock integration</option>
|
||||
<option value="true">enable sponsorblock integration</option>
|
||||
<option value="false">unset sponsorblock integration</option>
|
||||
<option value="null">unset sponsorblock integration</option>
|
||||
</select>
|
||||
</div>
|
||||
<h3>Page Size Overrides</h3>
|
||||
@ -301,7 +374,14 @@ const ChannelAbout = () => {
|
||||
recommended 50.
|
||||
</i>
|
||||
<br />
|
||||
<input type="number" name="channel_size" id="id_channel_size" />
|
||||
<input
|
||||
type="number"
|
||||
name="channel_size"
|
||||
id="id_channel_size"
|
||||
onChange={event => {
|
||||
setPageSizeVideo(Number(event.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<div className="overwrite-form-item">
|
||||
@ -316,7 +396,14 @@ const ChannelAbout = () => {
|
||||
max recommended 50.
|
||||
</i>
|
||||
<br />
|
||||
<input type="number" name="live_channel_size" id="id_live_channel_size" />
|
||||
<input
|
||||
type="number"
|
||||
name="live_channel_size"
|
||||
id="id_live_channel_size"
|
||||
onChange={event => {
|
||||
setPageSizeStreams(Number(event.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<div className="overwrite-form-item">
|
||||
@ -331,7 +418,14 @@ const ChannelAbout = () => {
|
||||
task, max recommended 50.
|
||||
</i>
|
||||
<br />
|
||||
<input type="number" name="shorts_channel_size" id="id_shorts_channel_size" />
|
||||
<input
|
||||
type="number"
|
||||
name="shorts_channel_size"
|
||||
id="id_shorts_channel_size"
|
||||
onChange={event => {
|
||||
setPageSizeShorts(Number(event.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
|
@ -7,6 +7,7 @@ import Notifications from '../components/Notifications';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ChannelBanner from '../components/ChannelBanner';
|
||||
import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
|
||||
import loadChannelById from '../api/loader/loadChannelById';
|
||||
|
||||
type ChannelParams = {
|
||||
channelId: string;
|
||||
@ -21,15 +22,19 @@ const ChannelBase = () => {
|
||||
const { channelId } = useParams() as ChannelParams;
|
||||
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||
|
||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||
const [channelNav, setChannelNav] = useState<ChannelNavResponseType>();
|
||||
const [startNotification, setStartNotification] = useState(false);
|
||||
|
||||
const channel = channelResponse?.data;
|
||||
const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const channelNavResponse = await loadChannelNav(channelId);
|
||||
const channelResponse = await loadChannelById(channelId);
|
||||
|
||||
setChannelResponse(channelResponse);
|
||||
setChannelNav(channelNavResponse);
|
||||
})();
|
||||
}, [channelId]);
|
||||
@ -43,7 +48,7 @@ const ChannelBase = () => {
|
||||
<div className="boxed-content">
|
||||
<div className="channel-banner">
|
||||
<Link to={Routes.ChannelVideo(channelId)}>
|
||||
<ChannelBanner channel_id={channelId} />
|
||||
<ChannelBanner channelId={channelId} channelBannerUrl={channel?.channel_banner_url} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="info-box-item child-page-nav">
|
||||
|
@ -7,7 +7,6 @@ import { useEffect, useState } from 'react';
|
||||
import { OutletContextType } from './Base';
|
||||
import Pagination from '../components/Pagination';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
||||
import { PlaylistsResponseType } from './Playlists';
|
||||
import iconGridView from '/img/icon-gridview.svg';
|
||||
@ -27,7 +26,7 @@ const ChannelPlaylist = () => {
|
||||
|
||||
const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false);
|
||||
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_playlist || 'grid');
|
||||
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
|
||||
const [gridItems] = useState(userMeConfig.grid_items || 3);
|
||||
const [refreshPlaylists, setRefreshPlaylists] = useState(false);
|
||||
|
||||
const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>();
|
||||
@ -53,9 +52,7 @@ const ChannelPlaylist = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Channel: Playlists</title>
|
||||
</Helmet>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className={`boxed-content ${gridView}`}>
|
||||
<Notifications pageName="channel" includeReindex={true} />
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const ChannelShorts = () => {
|
||||
const { channelId } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Channel: {channel.channel_name}</title>
|
||||
</Helmet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelShorts;
|
@ -1,16 +0,0 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const ChannelStream = () => {
|
||||
const { channelId } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Channel: {channel.channel_name}</title>
|
||||
</Helmet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelStream;
|
@ -20,11 +20,13 @@ 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,
|
||||
VideoTypes,
|
||||
} from '../api/loader/loadVideoListByPage';
|
||||
import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
|
||||
import humanFileSize from '../functions/humanFileSize';
|
||||
|
||||
type ChannelParams = {
|
||||
channelId: string;
|
||||
@ -34,7 +36,11 @@ type ChannelVideoLoaderType = {
|
||||
userConfig: UserMeType;
|
||||
};
|
||||
|
||||
const ChannelVideo = () => {
|
||||
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;
|
||||
@ -52,6 +58,7 @@ const ChannelVideo = () => {
|
||||
|
||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
||||
const [videoAggsResponse, setVideoAggsResponse] = useState<ChannelAggsType>();
|
||||
|
||||
const channel = channelResponse?.data;
|
||||
const videoList = videoResponse?.data;
|
||||
@ -78,10 +85,13 @@ const ChannelVideo = () => {
|
||||
watch: hideWatched ? 'unwatched' : undefined,
|
||||
sort: sortBy,
|
||||
order: sortOrder,
|
||||
type: videoType,
|
||||
});
|
||||
const channelAggs = await loadChannelAggs(channelId);
|
||||
|
||||
setChannelResponse(channelResponse);
|
||||
setVideoReponse(videos);
|
||||
setVideoAggsResponse(channelAggs);
|
||||
setRefresh(false);
|
||||
}
|
||||
})();
|
||||
@ -89,12 +99,6 @@ const ChannelVideo = () => {
|
||||
// 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">
|
||||
@ -106,9 +110,7 @@ const ChannelVideo = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Channel: {channel.channel_name}</title>
|
||||
</Helmet>
|
||||
<title>{`TA | Channel: ${channel.channel_name}`}</title>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className="boxed-content">
|
||||
<div className="info-box info-box-2">
|
||||
@ -117,17 +119,20 @@ const ChannelVideo = () => {
|
||||
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">
|
||||
{aggs && (
|
||||
{videoAggsResponse && (
|
||||
<>
|
||||
<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}
|
||||
{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
|
||||
|
@ -11,7 +11,6 @@ import { OutletContextType } from './Base';
|
||||
import ChannelList from '../components/ChannelList';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import Notifications from '../components/Notifications';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
|
||||
type ChannelOverwritesType = {
|
||||
@ -102,9 +101,7 @@ const Channels = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Channels</title>
|
||||
</Helmet>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className="boxed-content">
|
||||
<div className="title-split">
|
||||
|
@ -16,7 +16,6 @@ import updateDownloadQueue from '../api/actions/updateDownloadQueue';
|
||||
import updateTaskByName from '../api/actions/updateTaskByName';
|
||||
import Notifications from '../components/Notifications';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import DownloadListItem from '../components/DownloadListItem';
|
||||
import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs';
|
||||
@ -90,11 +89,6 @@ const Download = () => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (
|
||||
userMeConfig.show_ignored_only !== showIgnored ||
|
||||
userMeConfig.view_style_downloads !== view ||
|
||||
userMeConfig.grid_items !== gridItems
|
||||
) {
|
||||
const userConfig: UserConfigType = {
|
||||
show_ignored_only: showIgnored,
|
||||
[ViewStyleNames.downloads]: view,
|
||||
@ -103,7 +97,6 @@ const Download = () => {
|
||||
|
||||
await updateUserConfig(userConfig);
|
||||
setRefresh(true);
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
view,
|
||||
@ -152,9 +145,7 @@ const Download = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Downloads</title>
|
||||
</Helmet>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className="boxed-content">
|
||||
<div className="title-bar">
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
|
||||
|
||||
@ -16,9 +15,7 @@ const ErrorPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Oops!</title>
|
||||
</Helmet>
|
||||
|
||||
<div id="error-page" style={{ margin: '10%' }}>
|
||||
<h1>Oops!</h1>
|
||||
|
@ -13,7 +13,6 @@ import Filterbar from '../components/Filterbar';
|
||||
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { SponsorBlockType } from './Video';
|
||||
|
||||
export type PlayerType = {
|
||||
@ -169,9 +168,7 @@ const Home = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TubeArchivist</title>
|
||||
</Helmet>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className={`boxed-content ${gridView}`}>
|
||||
{continueVideos && continueVideos.length > 0 && (
|
||||
|
@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import signIn from '../api/actions/signIn';
|
||||
|
||||
@ -32,9 +31,7 @@ const Login = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Welcome</title>
|
||||
</Helmet>
|
||||
<div className="boxed-content login-page">
|
||||
<img alt="tube-archivist-logo" />
|
||||
<h1>Tube Archivist</h1>
|
||||
|
@ -28,7 +28,6 @@ import queueReindex from '../api/actions/queueReindex';
|
||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
|
||||
|
||||
@ -139,9 +138,7 @@ const Playlist = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Playlist: {playlist.playlist_name}</title>
|
||||
</Helmet>
|
||||
<title>{`TA | Playlist: ${playlist.playlist_name}`}</title>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className="boxed-content">
|
||||
<div className="title-bar">
|
||||
@ -154,6 +151,7 @@ const Playlist = () => {
|
||||
channelname={channel?.channel_name}
|
||||
channelSubs={channel?.channel_subs}
|
||||
channelSubscribed={channel?.channel_subscribed}
|
||||
channelThumbUrl={channel.channel_thumb_url}
|
||||
setRefresh={setRefresh}
|
||||
/>
|
||||
)}
|
||||
|
@ -15,7 +15,6 @@ import { PlaylistType } from './Playlist';
|
||||
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
||||
import createCustomPlaylist from '../api/actions/createCustomPlaylist';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
|
||||
export type PlaylistEntryType = {
|
||||
@ -68,6 +67,7 @@ const Playlists = () => {
|
||||
};
|
||||
|
||||
await updateUserConfig(userConfig);
|
||||
setRefresh(true);
|
||||
}
|
||||
})();
|
||||
}, [showSubedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_playlist, view]);
|
||||
@ -79,19 +79,22 @@ const Playlists = () => {
|
||||
pagination?.current_page === undefined ||
|
||||
currentPage !== pagination?.current_page
|
||||
) {
|
||||
const playlist = await loadPlaylistList({ page: currentPage });
|
||||
const playlist = await loadPlaylistList({
|
||||
page: currentPage,
|
||||
subscribed: showSubedOnly,
|
||||
});
|
||||
|
||||
setPlaylistReponse(playlist);
|
||||
setRefresh(false);
|
||||
}
|
||||
})();
|
||||
}, [refresh, currentPage, showSubedOnly, view, pagination?.current_page]);
|
||||
// Do not add showSubedOnly, view this will not work as expected!
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refresh, currentPage, pagination?.current_page]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Playlists</title>
|
||||
</Helmet>
|
||||
<ScrollToTopOnNavigate />
|
||||
<div className="boxed-content">
|
||||
<div className="title-split">
|
||||
|
@ -11,7 +11,6 @@ import PlaylistList from '../components/PlaylistList';
|
||||
import SubtitleList from '../components/SubtitleList';
|
||||
import { ViewStyles } from '../configuration/constants/ViewStyle';
|
||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import SearchExampleQueries from '../components/SearchExampleQueries';
|
||||
|
||||
const EmptySearchResponse: SearchResultsType = {
|
||||
@ -94,9 +93,7 @@ const Search = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TubeArchivist</title>
|
||||
</Helmet>
|
||||
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
||||
<div className={`boxed-content ${gridView}`}>
|
||||
<div className="title-bar">
|
||||
@ -106,7 +103,7 @@ const Search = () => {
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="searchInput"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
value={searchQuery}
|
||||
onChange={event => {
|
||||
|
@ -6,7 +6,6 @@ import updateTaskByName from '../api/actions/updateTaskByName';
|
||||
import queueBackup from '../api/actions/queueBackup';
|
||||
import restoreBackup from '../api/actions/restoreBackup';
|
||||
import Notifications from '../components/Notifications';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
|
||||
type Backup = {
|
||||
@ -43,9 +42,7 @@ const SettingsActions = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Actions</title>
|
||||
</Helmet>
|
||||
<div className="boxed-content">
|
||||
<SettingsNavigation />
|
||||
<Notifications
|
||||
|
@ -7,7 +7,6 @@ import restoreSnapshot from '../api/actions/restoreSnapshot';
|
||||
import queueSnapshot from '../api/actions/queueSnapshot';
|
||||
import updateCookie, { ValidatedCookieType } from '../api/actions/updateCookie';
|
||||
import deleteApiToken from '../api/actions/deleteApiToken';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import loadAppsettingsConfig, { AppSettingsConfigType } from '../api/loader/loadAppsettingsConfig';
|
||||
import updateAppsettingsConfig from '../api/actions/updateAppsettingsConfig';
|
||||
@ -33,102 +32,63 @@ type SnapshotListType = {
|
||||
type SettingsApplicationReponses = {
|
||||
snapshots?: SnapshotListType;
|
||||
appSettingsConfig?: AppSettingsConfigType;
|
||||
apiToken: string;
|
||||
apiToken?: string;
|
||||
};
|
||||
|
||||
const SettingsApplication = () => {
|
||||
const [response, setResponse] = useState<SettingsApplicationReponses>({
|
||||
snapshots: undefined,
|
||||
appSettingsConfig: undefined,
|
||||
apiToken: '',
|
||||
});
|
||||
const [response, setResponse] = useState<SettingsApplicationReponses>();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const snapshots = response?.snapshots;
|
||||
const appSettingsConfig = response?.appSettingsConfig;
|
||||
const apiToken = response.apiToken;
|
||||
const apiToken = response?.apiToken;
|
||||
|
||||
// Subscriptions
|
||||
const [videoPageSize, setVideoPageSize] = useState(
|
||||
appSettingsConfig?.subscriptions.channel_size || 0,
|
||||
);
|
||||
const [livePageSize, setLivePageSize] = useState(
|
||||
appSettingsConfig?.subscriptions.live_channel_size || 0,
|
||||
);
|
||||
const [shortPageSize, setShortPageSize] = useState(
|
||||
appSettingsConfig?.subscriptions.shorts_channel_size || 0,
|
||||
);
|
||||
const [isAutostart, setIsAutostart] = useState(
|
||||
appSettingsConfig?.subscriptions.auto_start || false,
|
||||
);
|
||||
const [videoPageSize, setVideoPageSize] = useState(0);
|
||||
const [livePageSize, setLivePageSize] = useState(0);
|
||||
const [shortPageSize, setShortPageSize] = useState(0);
|
||||
const [isAutostart, setIsAutostart] = useState(false);
|
||||
|
||||
// Downloads
|
||||
const [currentDownloadSpeed, setCurrentDownloadSpeed] = useState(
|
||||
appSettingsConfig?.downloads.limit_speed || 0,
|
||||
);
|
||||
const [currentThrottledRate, setCurrentThrottledRate] = useState(
|
||||
appSettingsConfig?.downloads.throttledratelimit || 0,
|
||||
);
|
||||
const [currentScrapingSleep, setCurrentScrapingSleep] = useState(
|
||||
appSettingsConfig?.downloads.sleep_interval || 0,
|
||||
);
|
||||
const [currentAutodelete, setCurrentAutodelete] = useState(
|
||||
appSettingsConfig?.downloads.autodelete_days || 0,
|
||||
);
|
||||
const [currentDownloadSpeed, setCurrentDownloadSpeed] = useState(0);
|
||||
const [currentThrottledRate, setCurrentThrottledRate] = useState(0);
|
||||
const [currentScrapingSleep, setCurrentScrapingSleep] = useState(0);
|
||||
const [currentAutodelete, setCurrentAutodelete] = useState(0);
|
||||
|
||||
// Download Format
|
||||
const [downloadsFormat, setDownloadsFormat] = useState(appSettingsConfig?.downloads.format || '');
|
||||
const [downloadsFormatSort, setDownloadsFormatSort] = useState(
|
||||
appSettingsConfig?.downloads.format_sort || '',
|
||||
);
|
||||
const [downloadsExtractorLang, setDownloadsExtractorLang] = useState(
|
||||
appSettingsConfig?.downloads.extractor_lang || '',
|
||||
);
|
||||
const [embedMetadata, setEmbedMetadata] = useState(
|
||||
appSettingsConfig?.downloads.add_metadata || false,
|
||||
);
|
||||
const [embedThumbnail, setEmbedThumbnail] = useState(
|
||||
appSettingsConfig?.downloads.add_thumbnail || false,
|
||||
);
|
||||
const [downloadsFormat, setDownloadsFormat] = useState('');
|
||||
const [downloadsFormatSort, setDownloadsFormatSort] = useState('');
|
||||
const [downloadsExtractorLang, setDownloadsExtractorLang] = useState('');
|
||||
const [embedMetadata, setEmbedMetadata] = useState(false);
|
||||
const [embedThumbnail, setEmbedThumbnail] = useState(false);
|
||||
|
||||
// Subtitles
|
||||
const [subtitleLang, setSubtitleLang] = useState(appSettingsConfig?.downloads.subtitle || '');
|
||||
const [subtitleSource, setSubtitleSource] = useState(
|
||||
appSettingsConfig?.downloads.subtitle_source || '',
|
||||
);
|
||||
const [indexSubtitles, setIndexSubtitles] = useState(
|
||||
appSettingsConfig?.downloads.subtitle_index || false,
|
||||
);
|
||||
const [subtitleLang, setSubtitleLang] = useState('');
|
||||
const [subtitleSource, setSubtitleSource] = useState('');
|
||||
const [indexSubtitles, setIndexSubtitles] = useState(false);
|
||||
|
||||
// Comments
|
||||
const [commentsMax, setCommentsMax] = useState(appSettingsConfig?.downloads.comment_max || 0);
|
||||
const [commentsSort, setCommentsSort] = useState(appSettingsConfig?.downloads.comment_sort || '');
|
||||
const [commentsMax, setCommentsMax] = useState('');
|
||||
const [commentsSort, setCommentsSort] = useState('');
|
||||
|
||||
// Cookie
|
||||
const [cookieImport, setCookieImport] = useState(
|
||||
appSettingsConfig?.downloads.cookie_import || false,
|
||||
);
|
||||
const [cookieImport, setCookieImport] = useState(false);
|
||||
const [validatingCookie, setValidatingCookie] = useState(false);
|
||||
const [cookieResponse, setCookieResponse] = useState<ValidatedCookieType>();
|
||||
|
||||
// Integrations
|
||||
const [showApiToken, setShowApiToken] = useState(false);
|
||||
const [downloadDislikes, setDownloadDislikes] = useState(
|
||||
appSettingsConfig?.downloads.integrate_ryd || false,
|
||||
);
|
||||
const [enableSponsorBlock, setEnableSponsorBlock] = useState(
|
||||
appSettingsConfig?.downloads.integrate_sponsorblock || false,
|
||||
);
|
||||
const [downloadDislikes, setDownloadDislikes] = useState(false);
|
||||
const [enableSponsorBlock, setEnableSponsorBlock] = useState(false);
|
||||
const [resetTokenResponse, setResetTokenResponse] = useState({});
|
||||
|
||||
// Snapshots
|
||||
const [enableSnapshots, setEnableSnapshots] = useState(
|
||||
appSettingsConfig?.application.enable_snapshot || false,
|
||||
);
|
||||
const [enableSnapshots, setEnableSnapshots] = useState(false);
|
||||
const [isSnapshotQueued, setIsSnapshotQueued] = useState(false);
|
||||
const [restoringSnapshot, setRestoringSnapshot] = useState(false);
|
||||
|
||||
const onSubmit = async () => {
|
||||
return await updateAppsettingsConfig({
|
||||
await updateAppsettingsConfig({
|
||||
application: {
|
||||
enable_snapshot: enableSnapshots,
|
||||
},
|
||||
@ -158,27 +118,74 @@ const SettingsApplication = () => {
|
||||
shorts_channel_size: shortPageSize,
|
||||
},
|
||||
});
|
||||
|
||||
setRefresh(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const fetchData = async () => {
|
||||
const snapshotResponse = await loadSnapshots();
|
||||
const appSettingsConfig = await loadAppsettingsConfig();
|
||||
const apiToken = await loadApiToken();
|
||||
|
||||
// Subscriptions
|
||||
setVideoPageSize(appSettingsConfig?.subscriptions.channel_size);
|
||||
setLivePageSize(appSettingsConfig?.subscriptions.live_channel_size);
|
||||
setShortPageSize(appSettingsConfig?.subscriptions.shorts_channel_size);
|
||||
setIsAutostart(appSettingsConfig?.subscriptions.auto_start);
|
||||
|
||||
// Downloads
|
||||
setCurrentDownloadSpeed(appSettingsConfig?.downloads.limit_speed || 0);
|
||||
setCurrentThrottledRate(appSettingsConfig?.downloads.throttledratelimit || 0);
|
||||
setCurrentScrapingSleep(appSettingsConfig?.downloads.sleep_interval);
|
||||
setCurrentAutodelete(appSettingsConfig?.downloads.autodelete_days);
|
||||
|
||||
// Download Format
|
||||
setDownloadsFormat(appSettingsConfig?.downloads.format.toString());
|
||||
setDownloadsFormatSort(appSettingsConfig?.downloads.format_sort.toString());
|
||||
setDownloadsExtractorLang(appSettingsConfig?.downloads.extractor_lang.toString());
|
||||
setEmbedMetadata(appSettingsConfig?.downloads.add_metadata);
|
||||
setEmbedThumbnail(appSettingsConfig?.downloads.add_thumbnail);
|
||||
|
||||
// Subtitles
|
||||
setSubtitleLang(appSettingsConfig?.downloads.subtitle.toString());
|
||||
setSubtitleSource(appSettingsConfig?.downloads.subtitle_source.toString());
|
||||
setIndexSubtitles(appSettingsConfig?.downloads.subtitle_index);
|
||||
|
||||
// Comments
|
||||
setCommentsMax(appSettingsConfig?.downloads.comment_max.toString());
|
||||
setCommentsSort(appSettingsConfig?.downloads.comment_sort);
|
||||
|
||||
// Cookie
|
||||
setCookieImport(appSettingsConfig?.downloads.cookie_import);
|
||||
|
||||
// Integrations
|
||||
setDownloadDislikes(appSettingsConfig?.downloads.integrate_ryd);
|
||||
setEnableSponsorBlock(appSettingsConfig?.downloads.integrate_sponsorblock);
|
||||
|
||||
// Snapshots
|
||||
setEnableSnapshots(appSettingsConfig?.application.enable_snapshot);
|
||||
|
||||
setResponse({
|
||||
snapshots: snapshotResponse,
|
||||
appSettingsConfig,
|
||||
apiToken: apiToken.token,
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (refresh) {
|
||||
fetchData();
|
||||
setRefresh(false);
|
||||
}
|
||||
}, [refresh]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Application Settings</title>
|
||||
</Helmet>
|
||||
<div className="boxed-content">
|
||||
<SettingsNavigation />
|
||||
<Notifications pageName={'all'} />
|
||||
@ -200,7 +207,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
YouTube page size:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.subscriptions.channel_size}
|
||||
{appSettingsConfig?.subscriptions.channel_size}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -211,7 +218,7 @@ const SettingsApplication = () => {
|
||||
<input
|
||||
type="number"
|
||||
name="subscriptions_channel_size"
|
||||
min="1"
|
||||
min="0"
|
||||
id="id_subscriptions_channel_size"
|
||||
value={videoPageSize}
|
||||
onChange={event => {
|
||||
@ -224,7 +231,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
YouTube Live page size:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.subscriptions.live_channel_size}
|
||||
{appSettingsConfig?.subscriptions.live_channel_size}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -248,7 +255,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
YouTube Shorts page size:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.subscriptions.shorts_channel_size}
|
||||
{appSettingsConfig?.subscriptions.shorts_channel_size}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -272,7 +279,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Auto start download from your subscriptions:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.subscriptions.auto_start}
|
||||
{appSettingsConfig?.subscriptions.auto_start.toString()}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -282,7 +289,6 @@ const SettingsApplication = () => {
|
||||
<select
|
||||
name="subscriptions_auto_start"
|
||||
id="id_subscriptions_auto_start"
|
||||
defaultValue=""
|
||||
value={isAutostart.toString()}
|
||||
onChange={event => {
|
||||
setIsAutostart(event.target.value === 'true');
|
||||
@ -300,9 +306,7 @@ const SettingsApplication = () => {
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Current download speed limit in KB/s:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.limit_speed}
|
||||
</span>
|
||||
<span className="settings-current">{appSettingsConfig?.downloads.limit_speed}</span>
|
||||
</p>
|
||||
<i>
|
||||
Limit download speed. 0 (zero) to deactivate, e.g. 1000 (1MB/s). Speeds are in KB/s.
|
||||
@ -324,7 +328,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Current throttled rate limit in KB/s:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.throttledratelimit}
|
||||
{appSettingsConfig?.downloads.throttledratelimit}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -347,7 +351,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Current scraping sleep interval:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.sleep_interval}
|
||||
{appSettingsConfig?.downloads.sleep_interval}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -371,7 +375,7 @@ const SettingsApplication = () => {
|
||||
<span className="danger-zone">Danger Zone</span>: Current auto delete watched
|
||||
videos:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.autodelete_days}
|
||||
{appSettingsConfig?.downloads.autodelete_days}
|
||||
</span>
|
||||
</p>
|
||||
<i>Auto delete watched videos after x days, 0 (zero) to deactivate:</i>
|
||||
@ -395,9 +399,7 @@ const SettingsApplication = () => {
|
||||
Limit video and audio quality format for yt-dlp.
|
||||
<br />
|
||||
Currently:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.format}
|
||||
</span>
|
||||
<span className="settings-current">{appSettingsConfig?.downloads.format}</span>
|
||||
</p>
|
||||
<p>Example configurations:</p>
|
||||
<ul>
|
||||
@ -450,9 +452,7 @@ const SettingsApplication = () => {
|
||||
Force sort order to have precedence over all yt-dlp fields.
|
||||
<br />
|
||||
Currently:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.format_sort}
|
||||
</span>
|
||||
<span className="settings-current">{appSettingsConfig?.downloads.format_sort}</span>
|
||||
</p>
|
||||
<p>Example configurations:</p>
|
||||
<ul>
|
||||
@ -490,7 +490,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Prefer translated metadata language:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.extractor_lang}
|
||||
{appSettingsConfig?.downloads.extractor_lang}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -518,7 +518,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Current metadata embed setting:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.add_metadata}
|
||||
{appSettingsConfig?.downloads.add_metadata.toString()}
|
||||
</span>
|
||||
</p>
|
||||
<i>Metadata is not embedded into the downloaded files by default.</i>
|
||||
@ -526,7 +526,6 @@ const SettingsApplication = () => {
|
||||
<select
|
||||
name="downloads_add_metadata"
|
||||
id="id_downloads_add_metadata"
|
||||
defaultValue=""
|
||||
value={embedMetadata.toString()}
|
||||
onChange={event => {
|
||||
setEmbedMetadata(event.target.value === 'true');
|
||||
@ -542,7 +541,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Current thumbnail embed setting:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.add_thumbnail}
|
||||
{appSettingsConfig?.downloads.add_thumbnail.toString()}
|
||||
</span>
|
||||
</p>
|
||||
<i>Embed thumbnail into the mediafile.</i>
|
||||
@ -568,9 +567,7 @@ const SettingsApplication = () => {
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Subtitles download setting:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.subtitle}
|
||||
</span>
|
||||
<span className="settings-current">{appSettingsConfig?.downloads.subtitle}</span>
|
||||
<br />
|
||||
<i>
|
||||
Choose which subtitles to download, add comma separated language codes,
|
||||
@ -593,7 +590,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Subtitle source settings:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.subtitle_source}
|
||||
{appSettingsConfig?.downloads.subtitle_source}
|
||||
</span>
|
||||
</p>
|
||||
<i>Download only user generated, or also less accurate auto generated subtitles.</i>
|
||||
@ -616,7 +613,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Index and make subtitles searchable:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.subtitle_index}
|
||||
{appSettingsConfig?.downloads.subtitle_index.toString()}
|
||||
</span>
|
||||
</p>
|
||||
<i>Store subtitle lines in Elasticsearch. Not recommended for low-end hardware.</i>
|
||||
@ -642,9 +639,7 @@ const SettingsApplication = () => {
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Download and index comments:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.comment_max}
|
||||
</span>
|
||||
<span className="settings-current">{appSettingsConfig?.downloads.comment_max}</span>
|
||||
<br />
|
||||
<i>
|
||||
Follow the yt-dlp max_comments documentation,{' '}
|
||||
@ -669,9 +664,9 @@ const SettingsApplication = () => {
|
||||
type="text"
|
||||
name="downloads_comment_max"
|
||||
id="id_downloads_comment_max"
|
||||
value={commentsMax.toString()}
|
||||
value={commentsMax}
|
||||
onChange={event => {
|
||||
setCommentsMax(Number(event.target.value));
|
||||
setCommentsMax(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
@ -680,7 +675,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Selected comment sort method:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.comment_sort}
|
||||
{appSettingsConfig?.downloads.comment_sort}
|
||||
</span>
|
||||
<br />
|
||||
<i>Select how many comments and threads to download:</i>
|
||||
@ -689,7 +684,7 @@ const SettingsApplication = () => {
|
||||
name="downloads_comment_sort"
|
||||
id="id_downloads_comment_sort"
|
||||
defaultValue=""
|
||||
value={commentsSort.toString()}
|
||||
value={commentsSort}
|
||||
onChange={event => {
|
||||
setCommentsSort(event.target.value);
|
||||
}}
|
||||
@ -708,7 +703,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Import YouTube cookie:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.cookie_import}
|
||||
{appSettingsConfig?.downloads.cookie_import}
|
||||
</span>
|
||||
<br />
|
||||
</p>
|
||||
@ -755,7 +750,7 @@ const SettingsApplication = () => {
|
||||
)}
|
||||
{!validatingCookie && (
|
||||
<>
|
||||
{appSettingsConfig && appSettingsConfig.downloads.cookie_import && (
|
||||
{appSettingsConfig?.downloads.cookie_import && (
|
||||
<div id="cookieMessage">
|
||||
<Button
|
||||
id="cookieButton"
|
||||
@ -813,7 +808,7 @@ const SettingsApplication = () => {
|
||||
</a>{' '}
|
||||
to get dislikes and average ratings back:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.integrate_ryd}
|
||||
{appSettingsConfig?.downloads.integrate_ryd.toString()}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -844,7 +839,7 @@ const SettingsApplication = () => {
|
||||
</a>{' '}
|
||||
to get sponsored timestamps:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.downloads.integrate_sponsorblock}
|
||||
{appSettingsConfig?.downloads.integrate_sponsorblock.toString()}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -874,7 +869,7 @@ const SettingsApplication = () => {
|
||||
<p>
|
||||
Current system snapshot:{' '}
|
||||
<span className="settings-current">
|
||||
{appSettingsConfig && appSettingsConfig.application.enable_snapshot}
|
||||
{appSettingsConfig?.application.enable_snapshot}
|
||||
</span>
|
||||
</p>
|
||||
<i>
|
||||
@ -955,6 +950,7 @@ const SettingsApplication = () => {
|
||||
name="application-settings"
|
||||
label="Update Application Configurations"
|
||||
onClick={async () => {
|
||||
window.scrollTo(0, 0);
|
||||
await onSubmit();
|
||||
}}
|
||||
/>
|
||||
|
@ -15,7 +15,6 @@ import DownloadHistoryStats from '../components/DownloadHistoryStats';
|
||||
import BiggestChannelsStats from '../components/BiggestChannelsStats';
|
||||
import Notifications from '../components/Notifications';
|
||||
import PaginationDummy from '../components/PaginationDummy';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export type VideoStatsType = {
|
||||
doc_count: number;
|
||||
@ -184,9 +183,7 @@ const SettingsDashboard = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Settings Dashboard</title>
|
||||
</Helmet>
|
||||
<div className="boxed-content">
|
||||
<SettingsNavigation />
|
||||
<Notifications pageName={'all'} />
|
||||
|
@ -1,148 +1,69 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Notifications from '../components/Notifications';
|
||||
import SettingsNavigation from '../components/SettingsNavigation';
|
||||
import Button from '../components/Button';
|
||||
|
||||
type CronTabType = {
|
||||
minute: number;
|
||||
hour: number;
|
||||
day_of_week: number;
|
||||
};
|
||||
|
||||
type SchedulerErrorType = {
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
type NotificationItemType = {
|
||||
task: string;
|
||||
notification: {
|
||||
title: string;
|
||||
urls: string[];
|
||||
};
|
||||
};
|
||||
|
||||
type SettingsSchedulingResponseType = {
|
||||
update_subscribed: {
|
||||
crontab: CronTabType;
|
||||
};
|
||||
check_reindex: {
|
||||
crontab: CronTabType;
|
||||
task_config: {
|
||||
days: 0;
|
||||
};
|
||||
};
|
||||
thumbnail_check: {
|
||||
crontab: CronTabType;
|
||||
};
|
||||
download_pending: {
|
||||
crontab: CronTabType;
|
||||
};
|
||||
run_backup: {
|
||||
crontab: CronTabType;
|
||||
task_config: {
|
||||
rotate: false;
|
||||
};
|
||||
};
|
||||
notifications: {
|
||||
items: NotificationItemType[];
|
||||
};
|
||||
scheduler_form: {
|
||||
update_subscribed: SchedulerErrorType;
|
||||
download_pending: SchedulerErrorType;
|
||||
check_reindex: SchedulerErrorType;
|
||||
thumbnail_check: SchedulerErrorType;
|
||||
run_backup: SchedulerErrorType;
|
||||
};
|
||||
};
|
||||
import PaginationDummy from '../components/PaginationDummy';
|
||||
import { useEffect, useState } from 'react';
|
||||
import loadSchedule, { ScheduleResponseType } from '../api/loader/loadSchedule';
|
||||
import loadAppriseNotification, {
|
||||
AppriseNotificationType,
|
||||
} from '../api/loader/loadAppriseNotification';
|
||||
import deleteTaskSchedule from '../api/actions/deleteTaskSchedule';
|
||||
import createTaskSchedule from '../api/actions/createTaskSchedule';
|
||||
import createAppriseNotificationUrl, {
|
||||
AppriseTaskNameType,
|
||||
} from '../api/actions/createAppriseNotificationUrl';
|
||||
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
||||
|
||||
const SettingsScheduling = () => {
|
||||
const response: SettingsSchedulingResponseType = {
|
||||
update_subscribed: {
|
||||
crontab: {
|
||||
minute: 0,
|
||||
hour: 0,
|
||||
day_of_week: 0,
|
||||
},
|
||||
},
|
||||
check_reindex: {
|
||||
crontab: {
|
||||
minute: 0,
|
||||
hour: 0,
|
||||
day_of_week: 0,
|
||||
},
|
||||
task_config: {
|
||||
days: 0,
|
||||
},
|
||||
},
|
||||
thumbnail_check: {
|
||||
crontab: {
|
||||
minute: 0,
|
||||
hour: 0,
|
||||
day_of_week: 0,
|
||||
},
|
||||
},
|
||||
download_pending: {
|
||||
crontab: {
|
||||
minute: 0,
|
||||
hour: 0,
|
||||
day_of_week: 0,
|
||||
},
|
||||
},
|
||||
run_backup: {
|
||||
crontab: {
|
||||
minute: 0,
|
||||
hour: 0,
|
||||
day_of_week: 0,
|
||||
},
|
||||
task_config: {
|
||||
rotate: false,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
items: [
|
||||
{
|
||||
task: '',
|
||||
notification: {
|
||||
title: '',
|
||||
urls: [''],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
scheduler_form: {
|
||||
update_subscribed: {
|
||||
errors: ['error?'],
|
||||
},
|
||||
download_pending: {
|
||||
errors: ['error?'],
|
||||
},
|
||||
check_reindex: {
|
||||
errors: ['error?'],
|
||||
},
|
||||
thumbnail_check: {
|
||||
errors: ['error?'],
|
||||
},
|
||||
run_backup: {
|
||||
errors: ['error?'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const {
|
||||
check_reindex,
|
||||
download_pending,
|
||||
notifications,
|
||||
run_backup,
|
||||
scheduler_form,
|
||||
thumbnail_check,
|
||||
update_subscribed,
|
||||
} = response;
|
||||
const [scheduleResponse, setScheduleResponse] = useState<ScheduleResponseType>([]);
|
||||
const [appriseNotification, setAppriseNotification] = useState<AppriseNotificationType>();
|
||||
|
||||
const [updateSubscribed, setUpdateSubscribed] = useState<string | undefined>();
|
||||
const [downloadPending, setDownloadPending] = useState<string | undefined>();
|
||||
const [checkReindex, setCheckReindex] = useState<string | undefined>();
|
||||
const [checkReindexDays, setCheckReindexDays] = useState<number | undefined>(undefined);
|
||||
const [thumbnailCheck, setThumbnailCheck] = useState<string | undefined>();
|
||||
const [zipBackup, setZipBackup] = useState<string | undefined>();
|
||||
const [zipBackupDays, setZipBackupDays] = useState<number | undefined>(undefined);
|
||||
const [notificationUrl, setNotificationUrl] = useState<string | undefined>(undefined);
|
||||
const [notificationTask, setNotificationTask] = useState<AppriseTaskNameType | string>('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (refresh) {
|
||||
const scheduleResponse = await loadSchedule();
|
||||
const appriseNotificationResponse = await loadAppriseNotification();
|
||||
|
||||
setScheduleResponse(scheduleResponse);
|
||||
setAppriseNotification(appriseNotificationResponse);
|
||||
|
||||
setRefresh(false);
|
||||
}
|
||||
})();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(true);
|
||||
}, []);
|
||||
|
||||
const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
|
||||
|
||||
console.log(groupedSchedules);
|
||||
|
||||
const { update_subscribed, download_pending, run_backup, check_reindex, thumbnail_check } =
|
||||
groupedSchedules;
|
||||
|
||||
const updateSubscribedSchedule = update_subscribed?.pop();
|
||||
const downloadPendingSchedule = download_pending?.pop();
|
||||
const runBackup = run_backup?.pop();
|
||||
const checkReindexSchedule = check_reindex?.pop();
|
||||
const thumbnailCheckSchedule = thumbnail_check?.pop();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | Scheduling Settings</title>
|
||||
</Helmet>
|
||||
<div className="boxed-content">
|
||||
<SettingsNavigation />
|
||||
<Notifications pageName={'all'} />
|
||||
@ -178,7 +99,7 @@ const SettingsScheduling = () => {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<form action="{% url 'settings_scheduling' %}" method="POST" name="scheduler-update">
|
||||
|
||||
<div className="settings-group">
|
||||
<h2>Rescan Subscriptions</h2>
|
||||
<div className="settings-item">
|
||||
@ -187,38 +108,51 @@ const SettingsScheduling = () => {
|
||||
<a href="https://members.tubearchivist.com/" target="_blank">
|
||||
members.tubearchivist.com
|
||||
</a>{' '}
|
||||
to get access to <span className="settings-current">real time</span> notifications
|
||||
for new videos uploaded by your favorite channels.
|
||||
to get access to <span className="settings-current">real time</span> notifications for
|
||||
new videos uploaded by your favorite channels.
|
||||
</p>
|
||||
<p>
|
||||
Current rescan schedule:{' '}
|
||||
<span className="settings-current">
|
||||
{update_subscribed && (
|
||||
{!updateSubscribedSchedule && 'False'}
|
||||
{updateSubscribedSchedule && (
|
||||
<>
|
||||
{update_subscribed.crontab.minute} {update_subscribed.crontab.hour}{' '}
|
||||
{update_subscribed.crontab.day_of_week}
|
||||
{updateSubscribedSchedule?.schedule}{' '}
|
||||
<Button
|
||||
label="Delete"
|
||||
data-schedule="update_subscribed"
|
||||
onclick="deleteSchedule(this)"
|
||||
onClick={async () => {
|
||||
await deleteTaskSchedule('update_subscribed');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
className="danger-button"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!update_subscribed && 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<p>Periodically rescan your subscriptions:</p>
|
||||
{scheduler_form.update_subscribed.errors.map(error => {
|
||||
return (
|
||||
<p key={error} className="danger-zone">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
|
||||
<input type="text" name="update_subscribed" id="id_update_subscribed" />
|
||||
<input
|
||||
type="text"
|
||||
value={updateSubscribed || updateSubscribedSchedule?.schedule}
|
||||
onChange={e => {
|
||||
setUpdateSubscribed(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('update_subscribed', {
|
||||
schedule: updateSubscribed,
|
||||
});
|
||||
|
||||
setUpdateSubscribed('');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-group">
|
||||
@ -227,109 +161,167 @@ const SettingsScheduling = () => {
|
||||
<p>
|
||||
Current Download schedule:{' '}
|
||||
<span className="settings-current">
|
||||
{download_pending && (
|
||||
{!download_pending && 'False'}
|
||||
{downloadPendingSchedule && (
|
||||
<>
|
||||
{download_pending.crontab.minute} {download_pending.crontab.hour}{' '}
|
||||
{download_pending.crontab.day_of_week}
|
||||
{downloadPendingSchedule?.schedule}{' '}
|
||||
<Button
|
||||
label="Delete"
|
||||
data-schedule="download_pending"
|
||||
onclick="deleteSchedule(this)"
|
||||
className="danger-button"
|
||||
onClick={async () => {
|
||||
await deleteTaskSchedule('download_pending');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!download_pending && 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<p>Automatic video download schedule:</p>
|
||||
{scheduler_form.download_pending.errors.map(error => {
|
||||
return (
|
||||
<p key={error} className="danger-zone">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
|
||||
<input type="text" name="download_pending" id="id_download_pending" />
|
||||
<input
|
||||
type="text"
|
||||
value={downloadPending || downloadPendingSchedule?.schedule}
|
||||
onChange={e => {
|
||||
setDownloadPending(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('download_pending', {
|
||||
schedule: downloadPending,
|
||||
});
|
||||
|
||||
setDownloadPending('');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h2>Refresh Metadata</h2>
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Current Metadata refresh schedule:{' '}
|
||||
<span className="settings-current">
|
||||
{check_reindex && (
|
||||
{!checkReindexSchedule && 'False'}
|
||||
{checkReindexSchedule && (
|
||||
<>
|
||||
{check_reindex.crontab.minute} {check_reindex.crontab.hour}{' '}
|
||||
{check_reindex.crontab.day_of_week}
|
||||
{checkReindexSchedule?.schedule}{' '}
|
||||
<Button
|
||||
label="Delete"
|
||||
data-schedule="check_reindex"
|
||||
onclick="deleteSchedule(this)"
|
||||
className="danger-button"
|
||||
onClick={async () => {
|
||||
await deleteTaskSchedule('check_reindex');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!check_reindex && 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<p>Daily schedule to refresh metadata from YouTube:</p>
|
||||
|
||||
<input type="text" name="check_reindex" id="id_check_reindex" />
|
||||
<input
|
||||
type="text"
|
||||
value={checkReindex || checkReindexSchedule?.schedule}
|
||||
onChange={e => {
|
||||
setCheckReindex(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('check_reindex', {
|
||||
schedule: checkReindex,
|
||||
});
|
||||
|
||||
setCheckReindex('');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Current refresh for metadata older than x days:{' '}
|
||||
<span className="settings-current">{check_reindex.task_config.days}</span>
|
||||
<span className="settings-current">{checkReindexSchedule?.config?.days}</span>
|
||||
</p>
|
||||
<p>Refresh older than x days, recommended 90:</p>
|
||||
{scheduler_form.check_reindex.errors.map(error => {
|
||||
return (
|
||||
<p key={error} className="danger-zone">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
|
||||
<input type="number" name="check_reindex_days" id="id_check_reindex_days" />
|
||||
<input
|
||||
type="number"
|
||||
value={checkReindexDays || checkReindexSchedule?.config?.days}
|
||||
onChange={e => {
|
||||
setCheckReindexDays(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('check_reindex', {
|
||||
config: {
|
||||
days: checkReindexDays,
|
||||
},
|
||||
});
|
||||
|
||||
setCheckReindexDays(undefined);
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
<h2>Thumbnail Check</h2>
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Current thumbnail check schedule:{' '}
|
||||
<span className="settings-current">
|
||||
{thumbnail_check && (
|
||||
{!thumbnailCheckSchedule && 'False'}
|
||||
{thumbnailCheckSchedule && (
|
||||
<>
|
||||
{thumbnail_check.crontab.minute} {thumbnail_check.crontab.hour}{' '}
|
||||
{thumbnail_check.crontab.day_of_week}
|
||||
{thumbnailCheckSchedule?.schedule}{' '}
|
||||
<Button
|
||||
label="Delete"
|
||||
data-schedule="thumbnail_check"
|
||||
onclick="deleteSchedule(this)"
|
||||
className="danger-button"
|
||||
onClick={async () => {
|
||||
await deleteTaskSchedule('thumbnail_check');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!thumbnail_check && 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<p>Periodically check and cleanup thumbnails:</p>
|
||||
{scheduler_form.thumbnail_check.errors.map(error => {
|
||||
return (
|
||||
<p key={error} className="danger-zone">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
|
||||
<input type="text" name="thumbnail_check" id="id_thumbnail_check" />
|
||||
<input
|
||||
type="text"
|
||||
value={thumbnailCheck || thumbnailCheckSchedule?.schedule}
|
||||
onChange={e => {
|
||||
setThumbnailCheck(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('thumbnail_check', {
|
||||
schedule: thumbnailCheck,
|
||||
});
|
||||
|
||||
setThumbnailCheck('');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-group">
|
||||
@ -337,82 +329,108 @@ const SettingsScheduling = () => {
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
<i>
|
||||
Zip file backups are very slow for large archives and consistency is not
|
||||
guaranteed, use snapshots instead. Make sure no other tasks are running when
|
||||
creating a Zip file backup.
|
||||
Zip file backups are very slow for large archives and consistency is not guaranteed,
|
||||
use snapshots instead. Make sure no other tasks are running when creating a Zip file
|
||||
backup.
|
||||
</i>
|
||||
</p>
|
||||
<p>
|
||||
Current index backup schedule:{' '}
|
||||
<span className="settings-current">
|
||||
{run_backup && (
|
||||
{!runBackup && 'False'}
|
||||
{runBackup && (
|
||||
<>
|
||||
{run_backup.crontab.minute} {run_backup.crontab.hour}{' '}
|
||||
{run_backup.crontab.day_of_week}
|
||||
{runBackup.schedule}{' '}
|
||||
<Button
|
||||
label="Delete"
|
||||
data-schedule="run_backup"
|
||||
onclick="deleteSchedule(this)"
|
||||
className="danger-button"
|
||||
onClick={async () => {
|
||||
await deleteTaskSchedule('run_backup');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!run_backup && 'False'}
|
||||
</span>
|
||||
</p>
|
||||
<p>Automatically backup metadata to a zip file:</p>
|
||||
{scheduler_form.run_backup.errors.map(error => {
|
||||
return (
|
||||
<p key={error} className="danger-zone">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
|
||||
<input type="text" name="run_backup" id="id_run_backup" />
|
||||
<input
|
||||
type="text"
|
||||
value={zipBackup || runBackup?.schedule}
|
||||
onChange={e => {
|
||||
setZipBackup(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('run_backup', {
|
||||
schedule: zipBackup,
|
||||
});
|
||||
|
||||
setZipBackup('');
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
Current backup files to keep:{' '}
|
||||
<span className="settings-current">{run_backup.task_config.rotate}</span>
|
||||
<span className="settings-current">{runBackup?.config?.rotate}</span>
|
||||
</p>
|
||||
<p>Max auto backups to keep:</p>
|
||||
|
||||
<input type="number" name="run_backup_rotate" id="id_run_backup_rotate" />
|
||||
<input
|
||||
type="number"
|
||||
value={(zipBackupDays || runBackup?.config?.rotate)?.toString()}
|
||||
onChange={e => {
|
||||
setZipBackupDays(Number(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createTaskSchedule('run_backup', {
|
||||
config: {
|
||||
rotate: zipBackupDays,
|
||||
},
|
||||
});
|
||||
|
||||
setZipBackupDays(undefined);
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-group">
|
||||
<h2>Add Notification URL</h2>
|
||||
<div className="settings-item">
|
||||
{notifications && (
|
||||
{!appriseNotification && <p>No notifications stored</p>}
|
||||
{appriseNotification && (
|
||||
<>
|
||||
<p>
|
||||
<Button
|
||||
label="Show"
|
||||
type="button"
|
||||
onclick="textReveal(this)"
|
||||
id="text-reveal-button"
|
||||
/>{' '}
|
||||
stored notification links
|
||||
</p>
|
||||
<div id="text-reveal" className="description-text">
|
||||
{notifications.items.map(({ task, notification }) => {
|
||||
<div className="description-text">
|
||||
{Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
|
||||
return (
|
||||
<>
|
||||
<h3 key={task}>{notification.title}</h3>
|
||||
{notification.urls.map((url: string) => {
|
||||
<h3 key={key}>{title}</h3>
|
||||
{urls.map((url: string) => {
|
||||
return (
|
||||
<p>
|
||||
<span>{url} </span>
|
||||
<Button
|
||||
type="button"
|
||||
className="danger-button"
|
||||
label="Delete"
|
||||
data-url={url}
|
||||
data-task={task}
|
||||
onclick="deleteNotificationUrl(this)"
|
||||
onClick={async () => {
|
||||
await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
<span> {url}</span>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
@ -422,8 +440,6 @@ const SettingsScheduling = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!notifications && <p>No notifications stored</p>}
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
@ -435,7 +451,13 @@ const SettingsScheduling = () => {
|
||||
library.
|
||||
</i>
|
||||
</p>
|
||||
<select name="task" id="id_task" defaultValue="">
|
||||
<select
|
||||
defaultValue=""
|
||||
value={notificationTask}
|
||||
onChange={e => {
|
||||
setNotificationTask(e.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
<option value="">-- select task --</option>
|
||||
<option value="update_subscribed">Rescan your Subscriptions</option>
|
||||
<option value="extract_download">Add to download queue</option>
|
||||
@ -445,15 +467,27 @@ const SettingsScheduling = () => {
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="notification_url"
|
||||
placeholder="Apprise notification URL"
|
||||
id="id_notification_url"
|
||||
value={notificationUrl}
|
||||
onChange={e => {
|
||||
setNotificationUrl(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createAppriseNotificationUrl(
|
||||
notificationTask as AppriseTaskNameType,
|
||||
notificationUrl || '',
|
||||
);
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" name="scheduler-settings" label="Update Scheduler Settings" />
|
||||
</form>
|
||||
<PaginationDummy />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -5,7 +5,6 @@ import loadUserMeConfig from '../api/loader/loadUserConfig';
|
||||
import { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
|
||||
import SettingsNavigation from '../components/SettingsNavigation';
|
||||
import Notifications from '../components/Notifications';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import { OutletContextType } from './Base';
|
||||
|
||||
@ -45,9 +44,7 @@ const SettingsUser = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | User Settings</title>
|
||||
</Helmet>
|
||||
<div className="boxed-content">
|
||||
<SettingsNavigation />
|
||||
<Notifications pageName={'all'} />
|
||||
|
@ -35,7 +35,6 @@ import updateCustomPlaylist from '../api/actions/updateCustomPlaylist';
|
||||
import { PlaylistType } from './Playlist';
|
||||
import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
|
||||
import CommentBox, { CommentsType } from '../components/CommentBox';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Button from '../components/Button';
|
||||
import { OutletContextType } from './Base';
|
||||
import getApiUrl from '../configuration/getApiUrl';
|
||||
@ -122,6 +121,14 @@ const Video = () => {
|
||||
const { videoId } = useParams() as VideoParams;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [videoEnded, setVideoEnded] = useState(false);
|
||||
const [playlistAutoplay, setPlaylistAutoplay] = useState(
|
||||
localStorage.getItem('playlistAutoplay') === 'true',
|
||||
);
|
||||
const [playlistIdForAutoplay, setPlaylistIDForAutoplay] = useState<string | undefined>(
|
||||
localStorage.getItem('playlistIdForAutoplay') ?? '',
|
||||
);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showAddToPlaylist, setShowAddToPlaylist] = useState(false);
|
||||
@ -137,6 +144,8 @@ const Video = () => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
|
||||
const videoResponse = await loadVideoById(videoId);
|
||||
const simmilarVideosResponse = await loadSimmilarVideosById(videoId);
|
||||
const videoProgressResponse = await loadVideoProgressById(videoId);
|
||||
@ -151,9 +160,40 @@ const Video = () => {
|
||||
setCustomPlaylistsResponse(customPlaylistsResponse);
|
||||
setCommentsResponse(commentsResponse);
|
||||
setRefreshVideoList(false);
|
||||
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [videoId, refreshVideoList]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('playlistAutoplay', playlistAutoplay.toString());
|
||||
|
||||
if (!playlistAutoplay) {
|
||||
localStorage.setItem('playlistIdForAutoplay', '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('playlistIdForAutoplay', playlistIdForAutoplay || '');
|
||||
}, [playlistAutoplay, playlistIdForAutoplay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoEnded && playlistAutoplay) {
|
||||
const playlist = videoPlaylistNav?.find(playlist => {
|
||||
return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
|
||||
});
|
||||
|
||||
if (playlist) {
|
||||
const nextYoutubeId = playlist.playlist_next?.youtube_id;
|
||||
|
||||
if (nextYoutubeId) {
|
||||
setVideoEnded(false);
|
||||
navigate(Routes.Video(nextYoutubeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [videoEnded, playlistAutoplay]);
|
||||
|
||||
if (videoResponse === undefined) {
|
||||
return [];
|
||||
}
|
||||
@ -173,16 +213,20 @@ const Video = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TA | {video.title}</title>
|
||||
</Helmet>
|
||||
<title>{`TA | ${video.title}`}</title>
|
||||
<ScrollToTopOnNavigate />
|
||||
|
||||
{!loading && (
|
||||
<VideoPlayer
|
||||
video={videoResponse}
|
||||
videoProgress={videoProgress}
|
||||
sponsorBlock={sponsorBlock}
|
||||
autoplay={playlistAutoplay}
|
||||
onVideoEnd={() => {
|
||||
setVideoEnded(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="boxed-content">
|
||||
<div className="title-bar">
|
||||
@ -203,6 +247,7 @@ const Video = () => {
|
||||
channelname={video.channel.channel_name}
|
||||
channelSubs={video.channel.channel_subs}
|
||||
channelSubscribed={video.channel.channel_subscribed}
|
||||
channelThumbUrl={video.channel.channel_thumb_url}
|
||||
setRefresh={setRefreshVideoList}
|
||||
/>
|
||||
|
||||
@ -219,7 +264,8 @@ const Video = () => {
|
||||
id: videoId,
|
||||
is_watched: status,
|
||||
});
|
||||
|
||||
}}
|
||||
onDone={() => {
|
||||
setRefreshVideoList(true);
|
||||
}}
|
||||
/>
|
||||
@ -438,6 +484,34 @@ const Video = () => {
|
||||
]: {playlistItem.playlist_meta.playlist_name}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
<div className="toggle">
|
||||
<p>Autoplay:</p>
|
||||
<div className="toggleBox">
|
||||
<input
|
||||
checked={playlistAutoplay}
|
||||
onChange={() => {
|
||||
if (!playlistAutoplay) {
|
||||
setPlaylistIDForAutoplay(playlistItem.playlist_meta.playlist_id);
|
||||
}
|
||||
|
||||
setPlaylistAutoplay(!playlistAutoplay);
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
{!playlistAutoplay && (
|
||||
<label htmlFor="" className="ofbtn">
|
||||
Off
|
||||
</label>
|
||||
)}
|
||||
{playlistAutoplay && (
|
||||
<label htmlFor="" className="onbtn">
|
||||
On
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="playlist-nav">
|
||||
<div className="playlist-nav-item">
|
||||
{playlistItem.playlist_previous && (
|
||||
|
@ -1361,3 +1361,45 @@ video:-webkit-full-screen {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/** loading indicator */
|
||||
/** source: https://github.com/loadingio/css-spinner/ */
|
||||
.lds-ring,
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 4px solid currentColor;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: currentColor transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user