run pre-commit on all
This commit is contained in:
parent
cf54f6d7fc
commit
bc74bf80f4
@ -18,4 +18,4 @@ venv/
|
|||||||
assets/*
|
assets/*
|
||||||
|
|
||||||
# for local testing only
|
# for local testing only
|
||||||
testing.sh
|
testing.sh
|
||||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1 @@
|
|||||||
docker_assets\run.sh eol=lf
|
docker_assets\run.sh eol=lf
|
||||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,3 +1,3 @@
|
|||||||
github: bbilly1
|
github: bbilly1
|
||||||
ko_fi: bbilly1
|
ko_fi: bbilly1
|
||||||
custom: https://paypal.me/bbilly1
|
custom: https://paypal.me/bbilly1
|
||||||
|
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
2
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
@ -6,7 +6,7 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: block
|
id: block
|
||||||
attributes:
|
attributes:
|
||||||
label: "This project doesn't accept any new feature requests for the forseeable future. There is no shortage of ideas and the next development steps are clear for years to come."
|
label: "This project doesn't accept any new feature requests for the foreseeable future. There is no shortage of ideas and the next development steps are clear for years to come."
|
||||||
options:
|
options:
|
||||||
- label: I understand that this issue will be closed without comment.
|
- label: I understand that this issue will be closed without comment.
|
||||||
required: true
|
required: true
|
||||||
|
48
.pre-commit-config.yaml
Normal file
48
.pre-commit-config.yaml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 24.10.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
alias: python
|
||||||
|
files: ^backend/
|
||||||
|
args: ["--line-length=79"]
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.13.2
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort (python)
|
||||||
|
alias: python
|
||||||
|
files: ^backend/
|
||||||
|
args: ["--profile", "black", "-l 79"]
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: 7.1.1
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
alias: python
|
||||||
|
files: ^backend/
|
||||||
|
args: [ "--max-complexity=10", "--max-line-length=79" ]
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
exclude: ^frontend/package-lock.json
|
||||||
|
# - repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
# rev: v9.17.0
|
||||||
|
# hooks:
|
||||||
|
# - id: eslint
|
||||||
|
# name: eslint
|
||||||
|
# entry: npm run --prefix ./frontend lint
|
||||||
|
# pass_filenames: false
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
entry: npm run --prefix ./frontend prettier
|
||||||
|
args: ["--write", "."]
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
exclude: '.*(\.svg|/migrations/).*'
|
@ -684,4 +684,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
black==24.10.0
|
|
||||||
codespell==2.3.0
|
|
||||||
flake8==7.1.1
|
|
||||||
ipython==8.31.0
|
ipython==8.31.0
|
||||||
isort==5.13.2
|
pre-commit==4.0.1
|
||||||
pylint-django==2.6.1
|
pylint-django==2.6.1
|
||||||
pylint==3.3.3
|
pylint==3.3.3
|
||||||
pytest-django==4.9.0
|
pytest-django==4.9.0
|
||||||
|
@ -25,7 +25,10 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="ID",
|
verbose_name="ID",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
(
|
||||||
|
"password",
|
||||||
|
models.CharField(max_length=128, verbose_name="password"),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"last_login",
|
"last_login",
|
||||||
models.DateTimeField(
|
models.DateTimeField(
|
||||||
|
@ -53,4 +53,4 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html =404;
|
try_files $uri $uri/ /index.html =404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
coverage
|
coverage
|
||||||
node_modules
|
node_modules
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
||||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#01202e" />
|
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#01202e" />
|
||||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||||
|
|
||||||
<meta name="apple-mobile-web-app-title" content="TubeArchivist" />
|
<meta name="apple-mobile-web-app-title" content="TubeArchivist" />
|
||||||
<meta name="application-name" content="TubeArchivist" />
|
<meta name="application-name" content="TubeArchivist" />
|
||||||
<meta name="msapplication-TileColor" content="#01202e" />
|
<meta name="msapplication-TileColor" content="#01202e" />
|
||||||
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
|
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
|
||||||
<meta name="theme-color" content="#01202e" />
|
<meta name="theme-color" content="#01202e" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>TubeArchivist</title>
|
<title>TubeArchivist</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
5906
frontend/package-lock.json
generated
5906
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,33 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "tubearchivist-frontend",
|
"name": "tubearchivist-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"build:deploy": "vite build",
|
"build:deploy": "vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"prettier": "prettier --write .",
|
||||||
},
|
"preview": "vite preview"
|
||||||
"dependencies": {
|
},
|
||||||
"dompurify": "^3.2.3",
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"dompurify": "^3.2.3",
|
||||||
"react-dom": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-dom": "^19.0.0",
|
||||||
"zustand": "^5.0.2"
|
"react-router-dom": "^7.0.2",
|
||||||
},
|
"zustand": "^5.0.2"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/react": "^19.0.1",
|
"devDependencies": {
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react": "^19.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@typescript-eslint/parser": "^8.18.0",
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@typescript-eslint/parser": "^8.18.0",
|
||||||
"eslint": "^9.16.0",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"prettier": "3.4.2",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"typescript": "^5.7.2",
|
"prettier": "3.4.2",
|
||||||
"vite": "^6.0.3"
|
"typescript": "^5.7.2",
|
||||||
}
|
"vite": "^6.0.3"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -1,42 +1,42 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import getCookie from '../../functions/getCookie';
|
import getCookie from '../../functions/getCookie';
|
||||||
|
|
||||||
const updateChannelSubscription = async (channelIds: string, status: boolean) => {
|
const updateChannelSubscription = async (channelIds: string, status: boolean) => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
const csrfCookie = getCookie('csrftoken');
|
const csrfCookie = getCookie('csrftoken');
|
||||||
|
|
||||||
const channels = [];
|
const channels = [];
|
||||||
const containsMultiple = channelIds.includes('\n');
|
const containsMultiple = channelIds.includes('\n');
|
||||||
|
|
||||||
if (containsMultiple) {
|
if (containsMultiple) {
|
||||||
const youtubeChannelIds = channelIds.split('\n');
|
const youtubeChannelIds = channelIds.split('\n');
|
||||||
|
|
||||||
youtubeChannelIds.forEach(channelId => {
|
youtubeChannelIds.forEach(channelId => {
|
||||||
channels.push({ channel_id: channelId, channel_subscribed: status });
|
channels.push({ channel_id: channelId, channel_subscribed: status });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
channels.push({ channel_id: channelIds, channel_subscribed: status });
|
channels.push({ channel_id: channelIds, channel_subscribed: status });
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/channel/`, {
|
const response = await fetch(`${apiUrl}/api/channel/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
'X-CSRFToken': csrfCookie || '',
|
'X-CSRFToken': csrfCookie || '',
|
||||||
},
|
},
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
data: [...channels],
|
data: [...channels],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const channelSubscription = await response.json();
|
const channelSubscription = await response.json();
|
||||||
console.log('updateChannelSubscription', channelSubscription);
|
console.log('updateChannelSubscription', channelSubscription);
|
||||||
|
|
||||||
return channelSubscription;
|
return channelSubscription;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updateChannelSubscription;
|
export default updateChannelSubscription;
|
||||||
|
@ -1,33 +1,33 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import getCookie from '../../functions/getCookie';
|
import getCookie from '../../functions/getCookie';
|
||||||
|
|
||||||
export type ValidatedCookieType = {
|
export type ValidatedCookieType = {
|
||||||
cookie_enabled: boolean;
|
cookie_enabled: boolean;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
validated: number;
|
validated: number;
|
||||||
validated_str: string;
|
validated_str: string;
|
||||||
cookie_validated?: boolean;
|
cookie_validated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCookie = async (): Promise<ValidatedCookieType> => {
|
const updateCookie = async (): Promise<ValidatedCookieType> => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
const csrfCookie = getCookie('csrftoken');
|
const csrfCookie = getCookie('csrftoken');
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/appsettings/cookie/`, {
|
const response = await fetch(`${apiUrl}/api/appsettings/cookie/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
'X-CSRFToken': csrfCookie || '',
|
'X-CSRFToken': csrfCookie || '',
|
||||||
},
|
},
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const validatedCookie = await response.json();
|
const validatedCookie = await response.json();
|
||||||
console.log('updateCookie', validatedCookie);
|
console.log('updateCookie', validatedCookie);
|
||||||
|
|
||||||
return validatedCookie;
|
return validatedCookie;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updateCookie;
|
export default updateCookie;
|
||||||
|
@ -1,47 +1,47 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import getCookie from '../../functions/getCookie';
|
import getCookie from '../../functions/getCookie';
|
||||||
|
|
||||||
const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean) => {
|
const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean) => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
const csrfCookie = getCookie('csrftoken');
|
const csrfCookie = getCookie('csrftoken');
|
||||||
|
|
||||||
const urls = [];
|
const urls = [];
|
||||||
const containsMultiple = youtubeIdStrings.includes('\n');
|
const containsMultiple = youtubeIdStrings.includes('\n');
|
||||||
|
|
||||||
if (containsMultiple) {
|
if (containsMultiple) {
|
||||||
const youtubeIds = youtubeIdStrings.split('\n');
|
const youtubeIds = youtubeIdStrings.split('\n');
|
||||||
|
|
||||||
youtubeIds.forEach(youtubeId => {
|
youtubeIds.forEach(youtubeId => {
|
||||||
urls.push({ youtube_id: youtubeId, status: 'pending' });
|
urls.push({ youtube_id: youtubeId, status: 'pending' });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
urls.push({ youtube_id: youtubeIdStrings, status: 'pending' });
|
urls.push({ youtube_id: youtubeIdStrings, status: 'pending' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = '';
|
let params = '';
|
||||||
if (autostart) {
|
if (autostart) {
|
||||||
params = '?autostart=true';
|
params = '?autostart=true';
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/download/${params}`, {
|
const response = await fetch(`${apiUrl}/api/download/${params}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
'X-CSRFToken': csrfCookie || '',
|
'X-CSRFToken': csrfCookie || '',
|
||||||
},
|
},
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
data: [...urls],
|
data: [...urls],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadState = await response.json();
|
const downloadState = await response.json();
|
||||||
console.log('updateDownloadQueue', downloadState);
|
console.log('updateDownloadQueue', downloadState);
|
||||||
|
|
||||||
return downloadState;
|
return downloadState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updateDownloadQueue;
|
export default updateDownloadQueue;
|
||||||
|
@ -1,42 +1,42 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import getCookie from '../../functions/getCookie';
|
import getCookie from '../../functions/getCookie';
|
||||||
|
|
||||||
const updatePlaylistSubscription = async (playlistIds: string, status: boolean) => {
|
const updatePlaylistSubscription = async (playlistIds: string, status: boolean) => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
const csrfCookie = getCookie('csrftoken');
|
const csrfCookie = getCookie('csrftoken');
|
||||||
|
|
||||||
const playlists = [];
|
const playlists = [];
|
||||||
const containsMultiple = playlistIds.includes('\n');
|
const containsMultiple = playlistIds.includes('\n');
|
||||||
|
|
||||||
if (containsMultiple) {
|
if (containsMultiple) {
|
||||||
const youtubePlaylistIds = playlistIds.split('\n');
|
const youtubePlaylistIds = playlistIds.split('\n');
|
||||||
|
|
||||||
youtubePlaylistIds.forEach(playlistId => {
|
youtubePlaylistIds.forEach(playlistId => {
|
||||||
playlists.push({ playlist_id: playlistId, playlist_subscribed: status });
|
playlists.push({ playlist_id: playlistId, playlist_subscribed: status });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
playlists.push({ playlist_id: playlistIds, playlist_subscribed: status });
|
playlists.push({ playlist_id: playlistIds, playlist_subscribed: status });
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/playlist/`, {
|
const response = await fetch(`${apiUrl}/api/playlist/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
'X-CSRFToken': csrfCookie || '',
|
'X-CSRFToken': csrfCookie || '',
|
||||||
},
|
},
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
data: [...playlists],
|
data: [...playlists],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const playlistSubscription = await response.json();
|
const playlistSubscription = await response.json();
|
||||||
console.log('updatePlaylistSubscription', playlistSubscription);
|
console.log('updatePlaylistSubscription', playlistSubscription);
|
||||||
|
|
||||||
return playlistSubscription;
|
return playlistSubscription;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updatePlaylistSubscription;
|
export default updatePlaylistSubscription;
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import getCookie from '../../functions/getCookie';
|
import getCookie from '../../functions/getCookie';
|
||||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||||
|
|
||||||
type ApiTokenResponse = {
|
type ApiTokenResponse = {
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadApiToken = async (): Promise<ApiTokenResponse> => {
|
const loadApiToken = async (): Promise<ApiTokenResponse> => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
const csrfCookie = getCookie('csrftoken');
|
const csrfCookie = getCookie('csrftoken');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
|
const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
'X-CSRFToken': csrfCookie || '',
|
'X-CSRFToken': csrfCookie || '',
|
||||||
},
|
},
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const apiToken = await response.json();
|
const apiToken = await response.json();
|
||||||
|
|
||||||
if (isDevEnvironment()) {
|
if (isDevEnvironment()) {
|
||||||
console.log('loadApiToken', apiToken);
|
console.log('loadApiToken', apiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiToken;
|
return apiToken;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { token: '' };
|
return { token: '' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default loadApiToken;
|
export default loadApiToken;
|
||||||
|
@ -1,54 +1,54 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||||
|
|
||||||
export type AppSettingsConfigType = {
|
export type AppSettingsConfigType = {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
channel_size: number;
|
channel_size: number;
|
||||||
live_channel_size: number;
|
live_channel_size: number;
|
||||||
shorts_channel_size: number;
|
shorts_channel_size: number;
|
||||||
auto_start: boolean;
|
auto_start: boolean;
|
||||||
};
|
};
|
||||||
downloads: {
|
downloads: {
|
||||||
limit_speed: false | number;
|
limit_speed: false | number;
|
||||||
sleep_interval: number;
|
sleep_interval: number;
|
||||||
autodelete_days: number;
|
autodelete_days: number;
|
||||||
format: number | string;
|
format: number | string;
|
||||||
format_sort: boolean | string;
|
format_sort: boolean | string;
|
||||||
add_metadata: boolean;
|
add_metadata: boolean;
|
||||||
add_thumbnail: boolean;
|
add_thumbnail: boolean;
|
||||||
subtitle: boolean | string;
|
subtitle: boolean | string;
|
||||||
subtitle_source: boolean | string;
|
subtitle_source: boolean | string;
|
||||||
subtitle_index: boolean;
|
subtitle_index: boolean;
|
||||||
comment_max: string | number;
|
comment_max: string | number;
|
||||||
comment_sort: string;
|
comment_sort: string;
|
||||||
cookie_import: boolean;
|
cookie_import: boolean;
|
||||||
throttledratelimit: false | number;
|
throttledratelimit: false | number;
|
||||||
extractor_lang: boolean | string;
|
extractor_lang: boolean | string;
|
||||||
integrate_ryd: boolean;
|
integrate_ryd: boolean;
|
||||||
integrate_sponsorblock: boolean;
|
integrate_sponsorblock: boolean;
|
||||||
};
|
};
|
||||||
application: {
|
application: {
|
||||||
enable_snapshot: boolean;
|
enable_snapshot: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAppsettingsConfig = async (): Promise<AppSettingsConfigType> => {
|
const loadAppsettingsConfig = async (): Promise<AppSettingsConfigType> => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/appsettings/config/`, {
|
const response = await fetch(`${apiUrl}/api/appsettings/config/`, {
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appSettingsConfig = await response.json();
|
const appSettingsConfig = await response.json();
|
||||||
|
|
||||||
if (isDevEnvironment()) {
|
if (isDevEnvironment()) {
|
||||||
console.log('loadApplicationConfig', appSettingsConfig);
|
console.log('loadApplicationConfig', appSettingsConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
return appSettingsConfig;
|
return appSettingsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default loadAppsettingsConfig;
|
export default loadAppsettingsConfig;
|
||||||
|
@ -1,74 +1,74 @@
|
|||||||
import defaultHeaders from '../../configuration/defaultHeaders';
|
import defaultHeaders from '../../configuration/defaultHeaders';
|
||||||
import getApiUrl from '../../configuration/getApiUrl';
|
import getApiUrl from '../../configuration/getApiUrl';
|
||||||
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
import getFetchCredentials from '../../configuration/getFetchCredentials';
|
||||||
import isDevEnvironment from '../../functions/isDevEnvironment';
|
import isDevEnvironment from '../../functions/isDevEnvironment';
|
||||||
import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
|
import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
|
||||||
import { PaginationType } from '../../components/Pagination';
|
import { PaginationType } from '../../components/Pagination';
|
||||||
|
|
||||||
export type VideoListByFilterResponseType = {
|
export type VideoListByFilterResponseType = {
|
||||||
data?: VideoType[];
|
data?: VideoType[];
|
||||||
config?: ConfigType;
|
config?: ConfigType;
|
||||||
paginate?: PaginationType;
|
paginate?: PaginationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WatchTypes = 'watched' | 'unwatched' | 'continue';
|
type WatchTypes = 'watched' | 'unwatched' | 'continue';
|
||||||
export type VideoTypes = 'videos' | 'streams' | 'shorts';
|
export type VideoTypes = 'videos' | 'streams' | 'shorts';
|
||||||
|
|
||||||
type FilterType = {
|
type FilterType = {
|
||||||
page?: number;
|
page?: number;
|
||||||
playlist?: string;
|
playlist?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
watch?: WatchTypes;
|
watch?: WatchTypes;
|
||||||
sort?: SortByType;
|
sort?: SortByType;
|
||||||
order?: SortOrderType;
|
order?: SortOrderType;
|
||||||
type?: VideoTypes;
|
type?: VideoTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadVideoListByFilter = async (
|
const loadVideoListByFilter = async (
|
||||||
filter: FilterType,
|
filter: FilterType,
|
||||||
): Promise<VideoListByFilterResponseType> => {
|
): Promise<VideoListByFilterResponseType> => {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
if (filter.page) {
|
if (filter.page) {
|
||||||
searchParams.append('page', filter.page.toString());
|
searchParams.append('page', filter.page.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.playlist) {
|
if (filter.playlist) {
|
||||||
searchParams.append('playlist', filter.playlist);
|
searchParams.append('playlist', filter.playlist);
|
||||||
} else if (filter.channel) {
|
} else if (filter.channel) {
|
||||||
searchParams.append('channel', filter.channel);
|
searchParams.append('channel', filter.channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.watch) {
|
if (filter.watch) {
|
||||||
searchParams.append('watch', filter.watch);
|
searchParams.append('watch', filter.watch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.sort) {
|
if (filter.sort) {
|
||||||
searchParams.append('sort', filter.sort);
|
searchParams.append('sort', filter.sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.order) {
|
if (filter.order) {
|
||||||
searchParams.append('order', filter.order);
|
searchParams.append('order', filter.order);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.type) {
|
if (filter.type) {
|
||||||
searchParams.append('type', filter.type);
|
searchParams.append('type', filter.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/video/?${searchParams.toString()}`, {
|
const response = await fetch(`${apiUrl}/api/video/?${searchParams.toString()}`, {
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
credentials: getFetchCredentials(),
|
credentials: getFetchCredentials(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const videos = await response.json();
|
const videos = await response.json();
|
||||||
|
|
||||||
if (isDevEnvironment()) {
|
if (isDevEnvironment()) {
|
||||||
console.log('loadVideoListByFilter', filter, videos);
|
console.log('loadVideoListByFilter', filter, videos);
|
||||||
}
|
}
|
||||||
|
|
||||||
return videos;
|
return videos;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default loadVideoListByFilter;
|
export default loadVideoListByFilter;
|
||||||
|
@ -1,42 +1,42 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface ButtonProps {
|
export interface ButtonProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: 'submit' | 'reset' | 'button' | undefined;
|
type?: 'submit' | 'reset' | 'button' | undefined;
|
||||||
label?: string | ReactNode | ReactNode[];
|
label?: string | ReactNode | ReactNode[];
|
||||||
children?: string | ReactNode | ReactNode[];
|
children?: string | ReactNode | ReactNode[];
|
||||||
value?: string;
|
value?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
className,
|
className,
|
||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
value,
|
value,
|
||||||
title,
|
title,
|
||||||
onClick,
|
onClick,
|
||||||
}: ButtonProps) => {
|
}: ButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
className={className}
|
className={className}
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import getApiUrl from '../configuration/getApiUrl';
|
import getApiUrl from '../configuration/getApiUrl';
|
||||||
import defaultChannelImage from '/img/default-channel-banner.jpg';
|
import defaultChannelImage from '/img/default-channel-banner.jpg';
|
||||||
|
|
||||||
type ChannelIconProps = {
|
type ChannelIconProps = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelBannerUrl: string | undefined;
|
channelBannerUrl: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
|
const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={`${getApiUrl()}${channelBannerUrl}`}
|
src={`${getApiUrl()}${channelBannerUrl}`}
|
||||||
alt={`${channelId}-banner`}
|
alt={`${channelId}-banner`}
|
||||||
onError={({ currentTarget }) => {
|
onError={({ currentTarget }) => {
|
||||||
currentTarget.onerror = null; // prevents looping
|
currentTarget.onerror = null; // prevents looping
|
||||||
currentTarget.src = defaultChannelImage;
|
currentTarget.src = defaultChannelImage;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChannelBanner;
|
export default ChannelBanner;
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import getApiUrl from '../configuration/getApiUrl';
|
import getApiUrl from '../configuration/getApiUrl';
|
||||||
import defaultChannelIcon from '/img/default-channel-icon.jpg';
|
import defaultChannelIcon from '/img/default-channel-icon.jpg';
|
||||||
|
|
||||||
type ChannelIconProps = {
|
type ChannelIconProps = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelThumbUrl: string | undefined;
|
channelThumbUrl: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
|
const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={`${getApiUrl()}${channelThumbUrl}`}
|
src={`${getApiUrl()}${channelThumbUrl}`}
|
||||||
alt={`${channelId}-thumb`}
|
alt={`${channelId}-thumb`}
|
||||||
onError={({ currentTarget }) => {
|
onError={({ currentTarget }) => {
|
||||||
currentTarget.onerror = null; // prevents looping
|
currentTarget.onerror = null; // prevents looping
|
||||||
currentTarget.src = defaultChannelIcon;
|
currentTarget.src = defaultChannelIcon;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChannelIcon;
|
export default ChannelIcon;
|
||||||
|
@ -1,98 +1,97 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ChannelType } from '../pages/Channels';
|
import { ChannelType } from '../pages/Channels';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
|
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
|
||||||
import formatDate from '../functions/formatDates';
|
import formatDate from '../functions/formatDates';
|
||||||
import FormattedNumber from './FormattedNumber';
|
import FormattedNumber from './FormattedNumber';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import ChannelIcon from './ChannelIcon';
|
import ChannelIcon from './ChannelIcon';
|
||||||
import ChannelBanner from './ChannelBanner';
|
import ChannelBanner from './ChannelBanner';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
type ChannelListProps = {
|
type ChannelListProps = {
|
||||||
channelList: ChannelType[] | undefined;
|
channelList: ChannelType[] | undefined;
|
||||||
refreshChannelList: (refresh: boolean) => void;
|
refreshChannelList: (refresh: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
|
const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
|
||||||
|
const { userConfig } = useUserConfigStore();
|
||||||
const { userConfig } = useUserConfigStore();
|
const viewLayout = userConfig.config.view_style_channel;
|
||||||
const viewLayout = userConfig.config.view_style_channel;
|
|
||||||
|
if (!channelList || channelList.length === 0) {
|
||||||
if (!channelList || channelList.length === 0) {
|
return <p>No channels found.</p>;
|
||||||
return <p>No channels found.</p>;
|
}
|
||||||
}
|
|
||||||
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
{channelList.map(channel => {
|
||||||
{channelList.map(channel => {
|
return (
|
||||||
return (
|
<div key={channel.channel_id} className={`channel-item ${viewLayout}`}>
|
||||||
<div key={channel.channel_id} className={`channel-item ${viewLayout}`}>
|
<div className={`channel-banner ${viewLayout}`}>
|
||||||
<div className={`channel-banner ${viewLayout}`}>
|
<Link to={Routes.Channel(channel.channel_id)}>
|
||||||
<Link to={Routes.Channel(channel.channel_id)}>
|
<ChannelBanner
|
||||||
<ChannelBanner
|
channelId={channel.channel_id}
|
||||||
channelId={channel.channel_id}
|
channelBannerUrl={channel.channel_banner_url}
|
||||||
channelBannerUrl={channel.channel_banner_url}
|
/>
|
||||||
/>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
<div className={`info-box info-box-2 ${viewLayout}`}>
|
||||||
<div className={`info-box info-box-2 ${viewLayout}`}>
|
<div className="info-box-item">
|
||||||
<div className="info-box-item">
|
<div className="round-img">
|
||||||
<div className="round-img">
|
<Link to={Routes.Channel(channel.channel_id)}>
|
||||||
<Link to={Routes.Channel(channel.channel_id)}>
|
<ChannelIcon
|
||||||
<ChannelIcon
|
channelId={channel.channel_id}
|
||||||
channelId={channel.channel_id}
|
channelThumbUrl={channel.channel_thumb_url}
|
||||||
channelThumbUrl={channel.channel_thumb_url}
|
/>
|
||||||
/>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<h3>
|
||||||
<h3>
|
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
|
||||||
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
|
</h3>
|
||||||
</h3>
|
<FormattedNumber text="Subscribers:" number={channel.channel_subs} />
|
||||||
<FormattedNumber text="Subscribers:" number={channel.channel_subs} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="info-box-item">
|
||||||
<div className="info-box-item">
|
<div>
|
||||||
<div>
|
<p>Last refreshed: {formatDate(channel.channel_last_refresh)}</p>
|
||||||
<p>Last refreshed: {formatDate(channel.channel_last_refresh)}</p>
|
{channel.channel_subscribed && (
|
||||||
{channel.channel_subscribed && (
|
<Button
|
||||||
<Button
|
label="Unsubscribe"
|
||||||
label="Unsubscribe"
|
className="unsubscribe"
|
||||||
className="unsubscribe"
|
type="button"
|
||||||
type="button"
|
title={`Unsubscribe from ${channel.channel_name}`}
|
||||||
title={`Unsubscribe from ${channel.channel_name}`}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await updateChannelSubscription(channel.channel_id, false);
|
||||||
await updateChannelSubscription(channel.channel_id, false);
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
refreshChannelList(true);
|
||||||
refreshChannelList(true);
|
}, 1000);
|
||||||
}, 1000);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
|
{!channel.channel_subscribed && (
|
||||||
{!channel.channel_subscribed && (
|
<Button
|
||||||
<Button
|
label="Subscribe"
|
||||||
label="Subscribe"
|
type="button"
|
||||||
type="button"
|
title={`Subscribe to ${channel.channel_name}`}
|
||||||
title={`Subscribe to ${channel.channel_name}`}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await updateChannelSubscription(channel.channel_id, true);
|
||||||
await updateChannelSubscription(channel.channel_id, true);
|
|
||||||
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
refreshChannelList(true);
|
||||||
refreshChannelList(true);
|
}, 500);
|
||||||
}, 500);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
export default ChannelList;
|
||||||
export default ChannelList;
|
|
||||||
|
@ -1,78 +1,78 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
|
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
|
||||||
import FormattedNumber from './FormattedNumber';
|
import FormattedNumber from './FormattedNumber';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import ChannelIcon from './ChannelIcon';
|
import ChannelIcon from './ChannelIcon';
|
||||||
|
|
||||||
type ChannelOverviewProps = {
|
type ChannelOverviewProps = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
channelname: string;
|
channelname: string;
|
||||||
channelSubs: number;
|
channelSubs: number;
|
||||||
channelSubscribed: boolean;
|
channelSubscribed: boolean;
|
||||||
channelThumbUrl: string;
|
channelThumbUrl: string;
|
||||||
showSubscribeButton?: boolean;
|
showSubscribeButton?: boolean;
|
||||||
isUserAdmin?: boolean;
|
isUserAdmin?: boolean;
|
||||||
setRefresh: (status: boolean) => void;
|
setRefresh: (status: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChannelOverview = ({
|
const ChannelOverview = ({
|
||||||
channelId,
|
channelId,
|
||||||
channelSubs,
|
channelSubs,
|
||||||
channelSubscribed,
|
channelSubscribed,
|
||||||
channelname,
|
channelname,
|
||||||
channelThumbUrl,
|
channelThumbUrl,
|
||||||
showSubscribeButton = false,
|
showSubscribeButton = false,
|
||||||
isUserAdmin,
|
isUserAdmin,
|
||||||
setRefresh,
|
setRefresh,
|
||||||
}: ChannelOverviewProps) => {
|
}: ChannelOverviewProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="info-box-item">
|
<div className="info-box-item">
|
||||||
<div className="round-img">
|
<div className="round-img">
|
||||||
<Link to={Routes.Channel(channelId)}>
|
<Link to={Routes.Channel(channelId)}>
|
||||||
<ChannelIcon channelId={channelId} channelThumbUrl={channelThumbUrl} />
|
<ChannelIcon channelId={channelId} channelThumbUrl={channelThumbUrl} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>
|
<h3>
|
||||||
<Link to={Routes.ChannelVideo(channelId)}>{channelname}</Link>
|
<Link to={Routes.ChannelVideo(channelId)}>{channelname}</Link>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<FormattedNumber text="Subscribers:" number={channelSubs} />
|
<FormattedNumber text="Subscribers:" number={channelSubs} />
|
||||||
|
|
||||||
{showSubscribeButton && (
|
{showSubscribeButton && (
|
||||||
<>
|
<>
|
||||||
{channelSubscribed && isUserAdmin && (
|
{channelSubscribed && isUserAdmin && (
|
||||||
<Button
|
<Button
|
||||||
label="Unsubscribe"
|
label="Unsubscribe"
|
||||||
className="unsubscribe"
|
className="unsubscribe"
|
||||||
type="button"
|
type="button"
|
||||||
title={`Unsubscribe from ${channelname}`}
|
title={`Unsubscribe from ${channelname}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateChannelSubscription(channelId, false);
|
await updateChannelSubscription(channelId, false);
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!channelSubscribed && (
|
{!channelSubscribed && (
|
||||||
<Button
|
<Button
|
||||||
label="Subscribe"
|
label="Subscribe"
|
||||||
type="button"
|
type="button"
|
||||||
title={`Subscribe to ${channelname}`}
|
title={`Subscribe to ${channelname}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateChannelSubscription(channelId, true);
|
await updateChannelSubscription(channelId, true);
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChannelOverview;
|
export default ChannelOverview;
|
||||||
|
@ -15,7 +15,6 @@ type DownloadListItemProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
|
||||||
|
|
||||||
const { userConfig } = useUserConfigStore();
|
const { userConfig } = useUserConfigStore();
|
||||||
const view = userConfig.config.view_style_downloads;
|
const view = userConfig.config.view_style_downloads;
|
||||||
const showIgnored = userConfig.config.show_ignored_only;
|
const showIgnored = userConfig.config.show_ignored_only;
|
||||||
|
@ -14,15 +14,10 @@ type FilterbarProps = {
|
|||||||
setRefresh?: (status: boolean) => void;
|
setRefresh?: (status: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Filterbar = ({
|
const Filterbar = ({ hideToggleText, viewStyleName, setRefresh }: FilterbarProps) => {
|
||||||
hideToggleText,
|
|
||||||
viewStyleName,
|
|
||||||
setRefresh,
|
|
||||||
}: FilterbarProps) => {
|
|
||||||
|
|
||||||
const { userConfig, setPartialConfig } = useUserConfigStore();
|
const { userConfig, setPartialConfig } = useUserConfigStore();
|
||||||
const [showHidden, setShowHidden] = useState(false);
|
const [showHidden, setShowHidden] = useState(false);
|
||||||
const isGridView = userConfig.config.view_style_home === ViewStyles.grid
|
const isGridView = userConfig.config.view_style_home === ViewStyles.grid;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="view-controls three">
|
<div className="view-controls three">
|
||||||
@ -35,7 +30,7 @@ const Filterbar = ({
|
|||||||
checked={userConfig.config.hide_watched}
|
checked={userConfig.config.hide_watched}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setRefresh?.(true);
|
setRefresh?.(true);
|
||||||
setPartialConfig({hide_watched: !userConfig.config.hide_watched})
|
setPartialConfig({ hide_watched: !userConfig.config.hide_watched });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -48,7 +43,6 @@ const Filterbar = ({
|
|||||||
Off
|
Off
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -62,7 +56,7 @@ const Filterbar = ({
|
|||||||
value={userConfig.config.sort_by}
|
value={userConfig.config.sort_by}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setRefresh?.(true);
|
setRefresh?.(true);
|
||||||
setPartialConfig({sort_by: event.target.value as SortByType});
|
setPartialConfig({ sort_by: event.target.value as SortByType });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="published">date published</option>
|
<option value="published">date published</option>
|
||||||
@ -78,7 +72,7 @@ const Filterbar = ({
|
|||||||
value={userConfig.config.sort_order}
|
value={userConfig.config.sort_order}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setRefresh?.(true);
|
setRefresh?.(true);
|
||||||
setPartialConfig({sort_order: event.target.value as SortOrderType})
|
setPartialConfig({ sort_order: event.target.value as SortOrderType });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="asc">asc</option>
|
<option value="asc">asc</option>
|
||||||
@ -106,7 +100,7 @@ const Filterbar = ({
|
|||||||
<img
|
<img
|
||||||
src={iconAdd}
|
src={iconAdd}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({grid_items: userConfig.config.grid_items + 1});
|
setPartialConfig({ grid_items: userConfig.config.grid_items + 1 });
|
||||||
}}
|
}}
|
||||||
alt="grid plus row"
|
alt="grid plus row"
|
||||||
/>
|
/>
|
||||||
@ -115,7 +109,7 @@ const Filterbar = ({
|
|||||||
<img
|
<img
|
||||||
src={iconSubstract}
|
src={iconSubstract}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({grid_items: userConfig.config.grid_items - 1});
|
setPartialConfig({ grid_items: userConfig.config.grid_items - 1 });
|
||||||
}}
|
}}
|
||||||
alt="grid minus row"
|
alt="grid minus row"
|
||||||
/>
|
/>
|
||||||
@ -125,14 +119,14 @@ const Filterbar = ({
|
|||||||
<img
|
<img
|
||||||
src={iconGridView}
|
src={iconGridView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({[viewStyleName]: 'grid'});
|
setPartialConfig({ [viewStyleName]: 'grid' });
|
||||||
}}
|
}}
|
||||||
alt="grid view"
|
alt="grid view"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={iconListView}
|
src={iconListView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({[viewStyleName]: 'list'});
|
setPartialConfig({ [viewStyleName]: 'list' });
|
||||||
}}
|
}}
|
||||||
alt="list view"
|
alt="list view"
|
||||||
/>
|
/>
|
||||||
|
@ -5,8 +5,8 @@ import { useAuthStore } from '../stores/AuthDataStore';
|
|||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const { auth } = useAuthStore();
|
const { auth } = useAuthStore();
|
||||||
const version = auth?.version
|
const version = auth?.version;
|
||||||
const taUpdate = auth?.ta_update
|
const taUpdate = auth?.ta_update;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
|
@ -1,226 +1,226 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { VideoType } from '../pages/Home';
|
import { VideoType } from '../pages/Home';
|
||||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||||
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
|
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
|
||||||
import watchedThreshold from '../functions/watchedThreshold';
|
import watchedThreshold from '../functions/watchedThreshold';
|
||||||
|
|
||||||
const getURL = () => {
|
const getURL = () => {
|
||||||
return window.location.origin;
|
return window.location.origin;
|
||||||
};
|
};
|
||||||
|
|
||||||
function shiftCurrentTime(contentCurrentTime: number | undefined) {
|
function shiftCurrentTime(contentCurrentTime: number | undefined) {
|
||||||
console.log(contentCurrentTime);
|
console.log(contentCurrentTime);
|
||||||
if (contentCurrentTime === undefined) {
|
if (contentCurrentTime === undefined) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shift media back 3 seconds to prevent missing some of the content
|
// Shift media back 3 seconds to prevent missing some of the content
|
||||||
if (contentCurrentTime > 5) {
|
if (contentCurrentTime > 5) {
|
||||||
return contentCurrentTime - 3;
|
return contentCurrentTime - 3;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function castVideoProgress(
|
async function castVideoProgress(
|
||||||
player: {
|
player: {
|
||||||
mediaInfo: { contentId: string | string[] };
|
mediaInfo: { contentId: string | string[] };
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
},
|
},
|
||||||
video: VideoType | undefined,
|
video: VideoType | undefined,
|
||||||
) {
|
) {
|
||||||
if (!video) {
|
if (!video) {
|
||||||
console.log('castVideoProgress: Video to cast not found...');
|
console.log('castVideoProgress: Video to cast not found...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const videoId = video.youtube_id;
|
const videoId = video.youtube_id;
|
||||||
|
|
||||||
if (player.mediaInfo.contentId.includes(videoId)) {
|
if (player.mediaInfo.contentId.includes(videoId)) {
|
||||||
const currentTime = player.currentTime;
|
const currentTime = player.currentTime;
|
||||||
const duration = player.duration;
|
const duration = player.duration;
|
||||||
|
|
||||||
if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
|
if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
|
||||||
// Check progress every 10 seconds or else progress is checked a few times a second
|
// Check progress every 10 seconds or else progress is checked a few times a second
|
||||||
await updateVideoProgressById({
|
await updateVideoProgressById({
|
||||||
youtubeId: videoId,
|
youtubeId: videoId,
|
||||||
currentProgress: currentTime,
|
currentProgress: currentTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!video.player.watched) {
|
if (!video.player.watched) {
|
||||||
// Check if video is already marked as watched
|
// Check if video is already marked as watched
|
||||||
if (watchedThreshold(currentTime, duration)) {
|
if (watchedThreshold(currentTime, duration)) {
|
||||||
await updateWatchedState({
|
await updateWatchedState({
|
||||||
id: videoId,
|
id: videoId,
|
||||||
is_watched: true,
|
is_watched: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function castVideoPaused(
|
async function castVideoPaused(
|
||||||
player: {
|
player: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
mediaInfo: { contentId: string | string[] } | null;
|
mediaInfo: { contentId: string | string[] } | null;
|
||||||
},
|
},
|
||||||
video: VideoType | undefined,
|
video: VideoType | undefined,
|
||||||
) {
|
) {
|
||||||
if (!video) {
|
if (!video) {
|
||||||
console.log('castVideoPaused: Video to cast not found...');
|
console.log('castVideoPaused: Video to cast not found...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoId = video?.youtube_id;
|
const videoId = video?.youtube_id;
|
||||||
|
|
||||||
const currentTime = player.currentTime;
|
const currentTime = player.currentTime;
|
||||||
const duration = player.duration;
|
const duration = player.duration;
|
||||||
|
|
||||||
if (player.mediaInfo != null) {
|
if (player.mediaInfo != null) {
|
||||||
if (player.mediaInfo.contentId.includes(videoId)) {
|
if (player.mediaInfo.contentId.includes(videoId)) {
|
||||||
if (currentTime !== 0 && duration !== 0) {
|
if (currentTime !== 0 && duration !== 0) {
|
||||||
await updateVideoProgressById({
|
await updateVideoProgressById({
|
||||||
youtubeId: videoId,
|
youtubeId: videoId,
|
||||||
currentProgress: currentTime,
|
currentProgress: currentTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GoogleCastProps = {
|
type GoogleCastProps = {
|
||||||
video?: VideoType;
|
video?: VideoType;
|
||||||
setRefresh?: () => void;
|
setRefresh?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GoogleCast = ({ video, setRefresh }: GoogleCastProps) => {
|
const GoogleCast = ({ video, setRefresh }: GoogleCastProps) => {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
const setup = useCallback(() => {
|
const setup = useCallback(() => {
|
||||||
const cast = globalThis.cast;
|
const cast = globalThis.cast;
|
||||||
const chrome = globalThis.chrome;
|
const chrome = globalThis.chrome;
|
||||||
|
|
||||||
cast.framework.CastContext.getInstance().setOptions({
|
cast.framework.CastContext.getInstance().setOptions({
|
||||||
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
|
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
|
||||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||||
});
|
});
|
||||||
|
|
||||||
const player = new cast.framework.RemotePlayer();
|
const player = new cast.framework.RemotePlayer();
|
||||||
|
|
||||||
const playerController = new cast.framework.RemotePlayerController(player);
|
const playerController = new cast.framework.RemotePlayerController(player);
|
||||||
|
|
||||||
// Add event listerner to check if a connection to a cast device is initiated
|
// Add event listerner to check if a connection to a cast device is initiated
|
||||||
playerController.addEventListener(
|
playerController.addEventListener(
|
||||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||||
function () {
|
function () {
|
||||||
setIsConnected(player.isConnected);
|
setIsConnected(player.isConnected);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
playerController.addEventListener(
|
playerController.addEventListener(
|
||||||
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
|
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
|
||||||
function () {
|
function () {
|
||||||
castVideoProgress(player, video);
|
castVideoProgress(player, video);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
playerController.addEventListener(
|
playerController.addEventListener(
|
||||||
cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
|
cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
|
||||||
function () {
|
function () {
|
||||||
castVideoPaused(player, video);
|
castVideoPaused(player, video);
|
||||||
setRefresh?.();
|
setRefresh?.();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [setRefresh, video]);
|
}, [setRefresh, video]);
|
||||||
|
|
||||||
const startPlayback = useCallback(() => {
|
const startPlayback = useCallback(() => {
|
||||||
const chrome = globalThis.chrome;
|
const chrome = globalThis.chrome;
|
||||||
const cast = globalThis.cast;
|
const cast = globalThis.cast;
|
||||||
const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
|
const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
|
||||||
|
|
||||||
const mediaUrl = video?.media_url;
|
const mediaUrl = video?.media_url;
|
||||||
const vidThumbUrl = video?.vid_thumb_url;
|
const vidThumbUrl = video?.vid_thumb_url;
|
||||||
const contentTitle = video?.title;
|
const contentTitle = video?.title;
|
||||||
const contentId = `${getURL()}${mediaUrl}`;
|
const contentId = `${getURL()}${mediaUrl}`;
|
||||||
const contentImage = `${getURL()}${vidThumbUrl}`;
|
const contentImage = `${getURL()}${vidThumbUrl}`;
|
||||||
const contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
|
const contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
|
||||||
|
|
||||||
const contentSubtitles = [];
|
const contentSubtitles = [];
|
||||||
const videoSubtitles = video?.subtitles; // Array of subtitles
|
const videoSubtitles = video?.subtitles; // Array of subtitles
|
||||||
if (typeof videoSubtitles !== 'undefined') {
|
if (typeof videoSubtitles !== 'undefined') {
|
||||||
for (let i = 0; i < videoSubtitles.length; i++) {
|
for (let i = 0; i < videoSubtitles.length; i++) {
|
||||||
const subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
|
const subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
|
||||||
|
|
||||||
subtitle.trackContentId = videoSubtitles[i].media_url;
|
subtitle.trackContentId = videoSubtitles[i].media_url;
|
||||||
subtitle.trackContentType = 'text/vtt';
|
subtitle.trackContentType = 'text/vtt';
|
||||||
subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
|
subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
|
||||||
subtitle.name = videoSubtitles[i].name;
|
subtitle.name = videoSubtitles[i].name;
|
||||||
subtitle.language = videoSubtitles[i].lang;
|
subtitle.language = videoSubtitles[i].lang;
|
||||||
subtitle.customData = null;
|
subtitle.customData = null;
|
||||||
|
|
||||||
contentSubtitles.push(subtitle);
|
contentSubtitles.push(subtitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
|
const mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
|
||||||
// mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
|
// mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
|
||||||
mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
|
mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
|
||||||
mediaInfo.metadata.title = contentTitle?.replace('&', '&'); // Set the video title
|
mediaInfo.metadata.title = contentTitle?.replace('&', '&'); // Set the video title
|
||||||
mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
|
mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
|
||||||
// mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
|
// mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
|
||||||
mediaInfo.tracks = contentSubtitles;
|
mediaInfo.tracks = contentSubtitles;
|
||||||
|
|
||||||
const request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
|
const request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
|
||||||
// request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
|
// request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
|
||||||
request.currentTime = shiftCurrentTime(video?.player?.position); // Set video start position based on the browser video position
|
request.currentTime = shiftCurrentTime(video?.player?.position); // Set video start position based on the browser video position
|
||||||
// request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
|
// request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
|
||||||
|
|
||||||
castSession.loadMedia(request).then(
|
castSession.loadMedia(request).then(
|
||||||
function () {
|
function () {
|
||||||
console.log('media loaded');
|
console.log('media loaded');
|
||||||
},
|
},
|
||||||
function (error: { code: string }) {
|
function (error: { code: string }) {
|
||||||
console.log('Error', error, 'Error code: ' + error.code);
|
console.log('Error', error, 'Error code: ' + error.code);
|
||||||
},
|
},
|
||||||
); // Send request to cast device
|
); // Send request to cast device
|
||||||
|
|
||||||
// Do not add videoProgress?.position, this will cause loops!
|
// Do not add videoProgress?.position, this will cause loops!
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [video?.media_url, video?.subtitles, video?.title, video?.vid_thumb_url]);
|
}, [video?.media_url, video?.subtitles, video?.title, video?.vid_thumb_url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// @ts-expect-error __onGCastApiAvailable is the google cast window hook ( source: https://developers.google.com/cast/docs/web_sender/integrate )
|
// @ts-expect-error __onGCastApiAvailable is the google cast window hook ( source: https://developers.google.com/cast/docs/web_sender/integrate )
|
||||||
window['__onGCastApiAvailable'] = function (isAvailable: boolean) {
|
window['__onGCastApiAvailable'] = function (isAvailable: boolean) {
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
setup();
|
setup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [setup]);
|
}, [setup]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('isConnected', isConnected);
|
console.log('isConnected', isConnected);
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
startPlayback();
|
startPlayback();
|
||||||
}
|
}
|
||||||
}, [isConnected, startPlayback]);
|
}, [isConnected, startPlayback]);
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return <p>Video for cast not found...</p>;
|
return <p>Video for cast not found...</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<>
|
<>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
{/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
|
{/* @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>
|
<google-cast-launcher id="castbutton"></google-cast-launcher>
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoogleCast;
|
export default GoogleCast;
|
||||||
|
@ -8,7 +8,6 @@ import logOut from '../api/actions/logOut';
|
|||||||
import loadIsAdmin from '../functions/getIsAdmin';
|
import loadIsAdmin from '../functions/getIsAdmin';
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
|
|
||||||
const isAdmin = loadIsAdmin();
|
const isAdmin = loadIsAdmin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleLogout = async (event: { preventDefault: () => void }) => {
|
const handleLogout = async (event: { preventDefault: () => void }) => {
|
||||||
|
@ -1,101 +1,101 @@
|
|||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import loadNotifications, { NotificationPages } from '../api/loader/loadNotifications';
|
import loadNotifications, { NotificationPages } from '../api/loader/loadNotifications';
|
||||||
import iconStop from '/img/icon-stop.svg';
|
import iconStop from '/img/icon-stop.svg';
|
||||||
import stopTaskByName from '../api/actions/stopTaskByName';
|
import stopTaskByName from '../api/actions/stopTaskByName';
|
||||||
|
|
||||||
type NotificationType = {
|
type NotificationType = {
|
||||||
title: string;
|
title: string;
|
||||||
group: string;
|
group: string;
|
||||||
api_stop: boolean;
|
api_stop: boolean;
|
||||||
level: string;
|
level: string;
|
||||||
id: string;
|
id: string;
|
||||||
command: boolean | string;
|
command: boolean | string;
|
||||||
messages: string[];
|
messages: string[];
|
||||||
progress: number;
|
progress: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NotificationResponseType = NotificationType[];
|
type NotificationResponseType = NotificationType[];
|
||||||
|
|
||||||
type NotificationsProps = {
|
type NotificationsProps = {
|
||||||
pageName: NotificationPages;
|
pageName: NotificationPages;
|
||||||
includeReindex?: boolean;
|
includeReindex?: boolean;
|
||||||
update?: boolean;
|
update?: boolean;
|
||||||
setShouldRefresh?: (isDone: boolean) => void;
|
setShouldRefresh?: (isDone: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Notifications = ({
|
const Notifications = ({
|
||||||
pageName,
|
pageName,
|
||||||
includeReindex = false,
|
includeReindex = false,
|
||||||
update,
|
update,
|
||||||
setShouldRefresh,
|
setShouldRefresh,
|
||||||
}: NotificationsProps) => {
|
}: NotificationsProps) => {
|
||||||
const [notificationResponse, setNotificationResponse] = useState<NotificationResponseType>([]);
|
const [notificationResponse, setNotificationResponse] = useState<NotificationResponseType>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(async () => {
|
const intervalId = setInterval(async () => {
|
||||||
const notifications = await loadNotifications(pageName, includeReindex);
|
const notifications = await loadNotifications(pageName, includeReindex);
|
||||||
|
|
||||||
if (notifications.length === 0) {
|
if (notifications.length === 0) {
|
||||||
setNotificationResponse(notifications);
|
setNotificationResponse(notifications);
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
setShouldRefresh?.(true);
|
setShouldRefresh?.(true);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
setShouldRefresh?.(false);
|
setShouldRefresh?.(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNotificationResponse(notifications);
|
setNotificationResponse(notifications);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [pageName, update, setShouldRefresh, includeReindex]);
|
}, [pageName, update, setShouldRefresh, includeReindex]);
|
||||||
|
|
||||||
if (notificationResponse.length === 0) {
|
if (notificationResponse.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{notificationResponse.map(notification => (
|
{notificationResponse.map(notification => (
|
||||||
<div
|
<div
|
||||||
id={notification.id}
|
id={notification.id}
|
||||||
className={`notification ${notification.level}`}
|
className={`notification ${notification.level}`}
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
>
|
>
|
||||||
<h3>{notification.title}</h3>
|
<h3>{notification.title}</h3>
|
||||||
<p>
|
<p>
|
||||||
{notification.messages.map?.(message => {
|
{notification.messages.map?.(message => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={message}>
|
<Fragment key={message}>
|
||||||
{message}
|
{message}
|
||||||
<br />
|
<br />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}) || notification.messages}
|
}) || notification.messages}
|
||||||
</p>
|
</p>
|
||||||
<div className="task-control-icons">
|
<div className="task-control-icons">
|
||||||
{notification['api_stop'] && notification.command !== 'STOP' && (
|
{notification['api_stop'] && notification.command !== 'STOP' && (
|
||||||
<img
|
<img
|
||||||
src={iconStop}
|
src={iconStop}
|
||||||
id="stop-icon"
|
id="stop-icon"
|
||||||
title="Stop Task"
|
title="Stop Task"
|
||||||
alt="stop icon"
|
alt="stop icon"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stopTaskByName(notification.id);
|
await stopTaskByName(notification.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="notification-progress-bar"
|
className="notification-progress-bar"
|
||||||
style={{ width: `${notification.progress * 100 || 0}%` }}
|
style={{ width: `${notification.progress * 100 || 0}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Notifications;
|
export default Notifications;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const PaginationDummy = () => {
|
const PaginationDummy = () => {
|
||||||
return (
|
return (
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<div className="pagination">{/** dummy pagination for consistent padding */}</div>
|
<div className="pagination">{/** dummy pagination for consistent padding */}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaginationDummy;
|
export default PaginationDummy;
|
||||||
|
@ -1,90 +1,89 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import { PlaylistType } from '../pages/Playlist';
|
import { PlaylistType } from '../pages/Playlist';
|
||||||
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
||||||
import formatDate from '../functions/formatDates';
|
import formatDate from '../functions/formatDates';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import PlaylistThumbnail from './PlaylistThumbnail';
|
import PlaylistThumbnail from './PlaylistThumbnail';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
type PlaylistListProps = {
|
type PlaylistListProps = {
|
||||||
playlistList: PlaylistType[] | undefined;
|
playlistList: PlaylistType[] | undefined;
|
||||||
setRefresh: (status: boolean) => void;
|
setRefresh: (status: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
|
const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
|
||||||
|
const { userConfig } = useUserConfigStore();
|
||||||
const { userConfig } = useUserConfigStore();
|
const viewLayout = userConfig.config.view_style_playlist;
|
||||||
const viewLayout = userConfig.config.view_style_playlist;
|
|
||||||
|
if (!playlistList || playlistList.length === 0) {
|
||||||
if (!playlistList || playlistList.length === 0) {
|
return <p>No playlists found.</p>;
|
||||||
return <p>No playlists found.</p>;
|
}
|
||||||
}
|
|
||||||
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
{playlistList.map((playlist: PlaylistType) => {
|
||||||
{playlistList.map((playlist: PlaylistType) => {
|
return (
|
||||||
return (
|
<div key={playlist.playlist_id} className={`playlist-item ${viewLayout}`}>
|
||||||
<div key={playlist.playlist_id} className={`playlist-item ${viewLayout}`}>
|
<div className="playlist-thumbnail">
|
||||||
<div className="playlist-thumbnail">
|
<Link to={Routes.Playlist(playlist.playlist_id)}>
|
||||||
<Link to={Routes.Playlist(playlist.playlist_id)}>
|
<PlaylistThumbnail
|
||||||
<PlaylistThumbnail
|
playlistId={playlist.playlist_id}
|
||||||
playlistId={playlist.playlist_id}
|
playlistThumbnail={playlist.playlist_thumbnail}
|
||||||
playlistThumbnail={playlist.playlist_thumbnail}
|
/>
|
||||||
/>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
<div className={`playlist-desc ${viewLayout}`}>
|
||||||
<div className={`playlist-desc ${viewLayout}`}>
|
{playlist.playlist_type != 'custom' && (
|
||||||
{playlist.playlist_type != 'custom' && (
|
<Link to={Routes.Channel(playlist.playlist_channel_id)}>
|
||||||
<Link to={Routes.Channel(playlist.playlist_channel_id)}>
|
<h3>{playlist.playlist_channel}</h3>
|
||||||
<h3>{playlist.playlist_channel}</h3>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
)}
|
|
||||||
|
<Link to={Routes.Playlist(playlist.playlist_id)}>
|
||||||
<Link to={Routes.Playlist(playlist.playlist_id)}>
|
<h2>{playlist.playlist_name}</h2>
|
||||||
<h2>{playlist.playlist_name}</h2>
|
</Link>
|
||||||
</Link>
|
|
||||||
|
<p>Last refreshed: {formatDate(playlist.playlist_last_refresh)}</p>
|
||||||
<p>Last refreshed: {formatDate(playlist.playlist_last_refresh)}</p>
|
|
||||||
|
{playlist.playlist_type != 'custom' && (
|
||||||
{playlist.playlist_type != 'custom' && (
|
<>
|
||||||
<>
|
{playlist.playlist_subscribed && (
|
||||||
{playlist.playlist_subscribed && (
|
<Button
|
||||||
<Button
|
label="Unsubscribe"
|
||||||
label="Unsubscribe"
|
className="unsubscribe"
|
||||||
className="unsubscribe"
|
type="button"
|
||||||
type="button"
|
title={`Unsubscribe from ${playlist.playlist_name}`}
|
||||||
title={`Unsubscribe from ${playlist.playlist_name}`}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await updatePlaylistSubscription(playlist.playlist_id, false);
|
||||||
await updatePlaylistSubscription(playlist.playlist_id, false);
|
|
||||||
|
setRefresh(true);
|
||||||
setRefresh(true);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
|
{!playlist.playlist_subscribed && (
|
||||||
{!playlist.playlist_subscribed && (
|
<Button
|
||||||
<Button
|
label="Subscribe"
|
||||||
label="Subscribe"
|
type="button"
|
||||||
type="button"
|
title={`Subscribe to ${playlist.playlist_name}`}
|
||||||
title={`Subscribe to ${playlist.playlist_name}`}
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await updatePlaylistSubscription(playlist.playlist_id, true);
|
||||||
await updatePlaylistSubscription(playlist.playlist_id, true);
|
|
||||||
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
setRefresh(true);
|
||||||
setRefresh(true);
|
}, 500);
|
||||||
}, 500);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
export default PlaylistList;
|
||||||
export default PlaylistList;
|
|
||||||
|
@ -1,262 +1,262 @@
|
|||||||
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
|
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
|
||||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||||
import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
|
import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
|
||||||
import watchedThreshold from '../functions/watchedThreshold';
|
import watchedThreshold from '../functions/watchedThreshold';
|
||||||
import Notifications from './Notifications';
|
import Notifications from './Notifications';
|
||||||
import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useState } from 'react';
|
import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useState } from 'react';
|
||||||
import formatTime from '../functions/formatTime';
|
import formatTime from '../functions/formatTime';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import getApiUrl from '../configuration/getApiUrl';
|
import getApiUrl from '../configuration/getApiUrl';
|
||||||
|
|
||||||
type VideoTag = SyntheticEvent<HTMLVideoElement, Event>;
|
type VideoTag = SyntheticEvent<HTMLVideoElement, Event>;
|
||||||
|
|
||||||
export type SkippedSegmentType = {
|
export type SkippedSegmentType = {
|
||||||
from: number;
|
from: number;
|
||||||
to: number;
|
to: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SponsorSegmentsSkippedType = Record<string, SkippedSegmentType>;
|
export type SponsorSegmentsSkippedType = Record<string, SkippedSegmentType>;
|
||||||
|
|
||||||
type Subtitle = {
|
type Subtitle = {
|
||||||
name: string;
|
name: string;
|
||||||
source: string;
|
source: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
media_url: string;
|
media_url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SubtitlesProp = {
|
type SubtitlesProp = {
|
||||||
subtitles: Subtitle[];
|
subtitles: Subtitle[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const Subtitles = ({ subtitles }: SubtitlesProp) => {
|
const Subtitles = ({ subtitles }: SubtitlesProp) => {
|
||||||
return subtitles.map((subtitle: Subtitle) => {
|
return subtitles.map((subtitle: Subtitle) => {
|
||||||
let label = subtitle.name;
|
let label = subtitle.name;
|
||||||
|
|
||||||
if (subtitle.source === 'auto') {
|
if (subtitle.source === 'auto') {
|
||||||
label += ' - auto';
|
label += ' - auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<track
|
<track
|
||||||
key={subtitle.name}
|
key={subtitle.name}
|
||||||
label={label}
|
label={label}
|
||||||
kind="subtitles"
|
kind="subtitles"
|
||||||
srcLang={subtitle.lang}
|
srcLang={subtitle.lang}
|
||||||
src={`${getApiUrl()}${subtitle.media_url}`}
|
src={`${getApiUrl()}${subtitle.media_url}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeUpdate =
|
const handleTimeUpdate =
|
||||||
(
|
(
|
||||||
youtubeId: string,
|
youtubeId: string,
|
||||||
duration: number,
|
duration: number,
|
||||||
watched: boolean,
|
watched: boolean,
|
||||||
sponsorBlock?: SponsorBlockType,
|
sponsorBlock?: SponsorBlockType,
|
||||||
setSponsorSegmentSkipped?: Dispatch<SetStateAction<SponsorSegmentsSkippedType>>,
|
setSponsorSegmentSkipped?: Dispatch<SetStateAction<SponsorSegmentsSkippedType>>,
|
||||||
) =>
|
) =>
|
||||||
async (videoTag: VideoTag) => {
|
async (videoTag: VideoTag) => {
|
||||||
const currentTime = Number(videoTag.currentTarget.currentTime);
|
const currentTime = Number(videoTag.currentTarget.currentTime);
|
||||||
|
|
||||||
if (sponsorBlock && sponsorBlock.segments) {
|
if (sponsorBlock && sponsorBlock.segments) {
|
||||||
sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
|
sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
|
||||||
const [from, to] = segment.segment;
|
const [from, to] = segment.segment;
|
||||||
|
|
||||||
if (currentTime >= from && currentTime <= from + 0.3) {
|
if (currentTime >= from && currentTime <= from + 0.3) {
|
||||||
videoTag.currentTarget.currentTime = to;
|
videoTag.currentTarget.currentTime = to;
|
||||||
|
|
||||||
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
||||||
return { ...segments, [segment.UUID]: { from, to } };
|
return { ...segments, [segment.UUID]: { from, to } };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTime > to + 10) {
|
if (currentTime > to + 10) {
|
||||||
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
||||||
return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
|
return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTime < 10) return;
|
if (currentTime < 10) return;
|
||||||
if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
|
if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
|
||||||
// Check progress every 10 seconds or else progress is checked a few times a second
|
// Check progress every 10 seconds or else progress is checked a few times a second
|
||||||
await updateVideoProgressById({
|
await updateVideoProgressById({
|
||||||
youtubeId,
|
youtubeId,
|
||||||
currentProgress: currentTime,
|
currentProgress: currentTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!watched) {
|
if (!watched) {
|
||||||
// Check if video is already marked as watched
|
// Check if video is already marked as watched
|
||||||
if (watchedThreshold(currentTime, duration)) {
|
if (watchedThreshold(currentTime, duration)) {
|
||||||
await updateWatchedState({
|
await updateWatchedState({
|
||||||
id: youtubeId,
|
id: youtubeId,
|
||||||
is_watched: true,
|
is_watched: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type VideoPlayerProps = {
|
type VideoPlayerProps = {
|
||||||
video: VideoResponseType;
|
video: VideoResponseType;
|
||||||
sponsorBlock?: SponsorBlockType;
|
sponsorBlock?: SponsorBlockType;
|
||||||
embed?: boolean;
|
embed?: boolean;
|
||||||
autoplay?: boolean;
|
autoplay?: boolean;
|
||||||
onVideoEnd?: () => void;
|
onVideoEnd?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VideoPlayer = ({
|
const VideoPlayer = ({
|
||||||
video,
|
video,
|
||||||
sponsorBlock,
|
sponsorBlock,
|
||||||
embed,
|
embed,
|
||||||
autoplay = false,
|
autoplay = false,
|
||||||
onVideoEnd,
|
onVideoEnd,
|
||||||
}: VideoPlayerProps) => {
|
}: VideoPlayerProps) => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const searchParamVideoProgress = searchParams.get('t');
|
const searchParamVideoProgress = searchParams.get('t');
|
||||||
|
|
||||||
const [skippedSegments, setSkippedSegments] = useState<SponsorSegmentsSkippedType>({});
|
const [skippedSegments, setSkippedSegments] = useState<SponsorSegmentsSkippedType>({});
|
||||||
|
|
||||||
const videoId = video.data.youtube_id;
|
const videoId = video.data.youtube_id;
|
||||||
const videoUrl = video.data.media_url;
|
const videoUrl = video.data.media_url;
|
||||||
const videoThumbUrl = video.data.vid_thumb_url;
|
const videoThumbUrl = video.data.vid_thumb_url;
|
||||||
const watched = video.data.player.watched;
|
const watched = video.data.player.watched;
|
||||||
const duration = video.data.player.duration;
|
const duration = video.data.player.duration;
|
||||||
const videoSubtitles = video.data.subtitles;
|
const videoSubtitles = video.data.subtitles;
|
||||||
|
|
||||||
let videoSrcProgress =
|
let videoSrcProgress =
|
||||||
Number(video.data.player?.position) > 0 ? Number(video.data.player?.position) : '';
|
Number(video.data.player?.position) > 0 ? Number(video.data.player?.position) : '';
|
||||||
|
|
||||||
if (searchParamVideoProgress !== null) {
|
if (searchParamVideoProgress !== null) {
|
||||||
videoSrcProgress = searchParamVideoProgress;
|
videoSrcProgress = searchParamVideoProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleVideoEnd =
|
const handleVideoEnd =
|
||||||
(
|
(
|
||||||
youtubeId: string,
|
youtubeId: string,
|
||||||
watched: boolean,
|
watched: boolean,
|
||||||
setSponsorSegmentSkipped?: Dispatch<SetStateAction<SponsorSegmentsSkippedType>>,
|
setSponsorSegmentSkipped?: Dispatch<SetStateAction<SponsorSegmentsSkippedType>>,
|
||||||
) =>
|
) =>
|
||||||
async () => {
|
async () => {
|
||||||
if (!watched) {
|
if (!watched) {
|
||||||
// Check if video is already marked as watched
|
// Check if video is already marked as watched
|
||||||
await updateWatchedState({ id: youtubeId, is_watched: true });
|
await updateWatchedState({ id: youtubeId, is_watched: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
|
||||||
const keys = Object.keys(segments);
|
const keys = Object.keys(segments);
|
||||||
|
|
||||||
keys.forEach(uuid => {
|
keys.forEach(uuid => {
|
||||||
segments[uuid] = { from: 0, to: 0 };
|
segments[uuid] = { from: 0, to: 0 };
|
||||||
});
|
});
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
});
|
});
|
||||||
|
|
||||||
onVideoEnd?.();
|
onVideoEnd?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="player" className={embed ? '' : 'player-wrapper'}>
|
<div id="player" className={embed ? '' : 'player-wrapper'}>
|
||||||
<div className={embed ? '' : 'video-main'}>
|
<div className={embed ? '' : 'video-main'}>
|
||||||
<video
|
<video
|
||||||
poster={`${getApiUrl()}${videoThumbUrl}`}
|
poster={`${getApiUrl()}${videoThumbUrl}`}
|
||||||
onVolumeChange={(videoTag: VideoTag) => {
|
onVolumeChange={(videoTag: VideoTag) => {
|
||||||
localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
|
localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
|
||||||
}}
|
}}
|
||||||
onRateChange={(videoTag: VideoTag) => {
|
onRateChange={(videoTag: VideoTag) => {
|
||||||
localStorage.setItem('playerSpeed', videoTag.currentTarget.playbackRate.toString());
|
localStorage.setItem('playerSpeed', videoTag.currentTarget.playbackRate.toString());
|
||||||
}}
|
}}
|
||||||
onLoadStart={(videoTag: VideoTag) => {
|
onLoadStart={(videoTag: VideoTag) => {
|
||||||
videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume') ?? 1);
|
videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume') ?? 1);
|
||||||
videoTag.currentTarget.playbackRate = Number(
|
videoTag.currentTarget.playbackRate = Number(
|
||||||
localStorage.getItem('playerSpeed') ?? 1,
|
localStorage.getItem('playerSpeed') ?? 1,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onTimeUpdate={handleTimeUpdate(
|
onTimeUpdate={handleTimeUpdate(
|
||||||
videoId,
|
videoId,
|
||||||
duration,
|
duration,
|
||||||
watched,
|
watched,
|
||||||
sponsorBlock,
|
sponsorBlock,
|
||||||
setSkippedSegments,
|
setSkippedSegments,
|
||||||
)}
|
)}
|
||||||
onPause={async (videoTag: VideoTag) => {
|
onPause={async (videoTag: VideoTag) => {
|
||||||
const currentTime = Number(videoTag.currentTarget.currentTime);
|
const currentTime = Number(videoTag.currentTarget.currentTime);
|
||||||
|
|
||||||
if (currentTime < 10) return;
|
if (currentTime < 10) return;
|
||||||
|
|
||||||
await updateVideoProgressById({
|
await updateVideoProgressById({
|
||||||
youtubeId: videoId,
|
youtubeId: videoId,
|
||||||
currentProgress: currentTime,
|
currentProgress: currentTime,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onEnded={handleVideoEnd(videoId, watched)}
|
onEnded={handleVideoEnd(videoId, watched)}
|
||||||
autoPlay={autoplay}
|
autoPlay={autoplay}
|
||||||
controls
|
controls
|
||||||
width="100%"
|
width="100%"
|
||||||
playsInline
|
playsInline
|
||||||
id="video-item"
|
id="video-item"
|
||||||
>
|
>
|
||||||
<source
|
<source
|
||||||
src={`${getApiUrl()}${videoUrl}#t=${videoSrcProgress}`}
|
src={`${getApiUrl()}${videoUrl}#t=${videoSrcProgress}`}
|
||||||
type="video/mp4"
|
type="video/mp4"
|
||||||
id="video-source"
|
id="video-source"
|
||||||
/>
|
/>
|
||||||
{videoSubtitles && <Subtitles subtitles={videoSubtitles} />}
|
{videoSubtitles && <Subtitles subtitles={videoSubtitles} />}
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Notifications pageName="all" />
|
<Notifications pageName="all" />
|
||||||
<div className="sponsorblock" id="sponsorblock">
|
<div className="sponsorblock" id="sponsorblock">
|
||||||
{sponsorBlock?.is_enabled && (
|
{sponsorBlock?.is_enabled && (
|
||||||
<>
|
<>
|
||||||
{sponsorBlock.segments.length == 0 && (
|
{sponsorBlock.segments.length == 0 && (
|
||||||
<h4>
|
<h4>
|
||||||
This video doesn't have any sponsor segments added. To add a segment go to{' '}
|
This video doesn't have any sponsor segments added. To add a segment go to{' '}
|
||||||
<u>
|
<u>
|
||||||
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a>
|
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a>
|
||||||
</u>{' '}
|
</u>{' '}
|
||||||
and add a segment using the{' '}
|
and add a segment using the{' '}
|
||||||
<u>
|
<u>
|
||||||
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
||||||
</u>{' '}
|
</u>{' '}
|
||||||
extension.
|
extension.
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
{sponsorBlock.has_unlocked && (
|
{sponsorBlock.has_unlocked && (
|
||||||
<h4>
|
<h4>
|
||||||
This video has unlocked sponsor segments. Go to{' '}
|
This video has unlocked sponsor segments. Go to{' '}
|
||||||
<u>
|
<u>
|
||||||
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a>
|
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a>
|
||||||
</u>{' '}
|
</u>{' '}
|
||||||
and vote on the segments using the{' '}
|
and vote on the segments using the{' '}
|
||||||
<u>
|
<u>
|
||||||
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
||||||
</u>{' '}
|
</u>{' '}
|
||||||
extension.
|
extension.
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{Object.values(skippedSegments).map(({ from, to }, index) => {
|
{Object.values(skippedSegments).map(({ from, to }, index) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${from}-${to}-${index}`}>
|
<Fragment key={`${from}-${to}-${index}`}>
|
||||||
{from !== 0 && to !== 0 && (
|
{from !== 0 && to !== 0 && (
|
||||||
<h3>
|
<h3>
|
||||||
Skipped sponsor segment from {formatTime(from)} to {formatTime(to)}.
|
Skipped sponsor segment from {formatTime(from)} to {formatTime(to)}.
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoPlayer;
|
export default VideoPlayer;
|
||||||
|
@ -8,9 +8,8 @@ export const ColourConstant = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const importColours = () => {
|
const importColours = () => {
|
||||||
|
|
||||||
const { userConfig } = useUserConfigStore();
|
const { userConfig } = useUserConfigStore();
|
||||||
const stylesheet = userConfig?.config.stylesheet
|
const stylesheet = userConfig?.config.stylesheet;
|
||||||
|
|
||||||
switch (stylesheet) {
|
switch (stylesheet) {
|
||||||
case ColourConstant.Dark:
|
case ColourConstant.Dark:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
const loadIsAdmin = () => {
|
const loadIsAdmin = () => {
|
||||||
const { userConfig } = useUserConfigStore()
|
const { userConfig } = useUserConfigStore();
|
||||||
const isAdmin = userConfig?.is_staff || userConfig?.is_superuser;
|
const isAdmin = userConfig?.is_staff || userConfig?.is_superuser;
|
||||||
|
|
||||||
return isAdmin;
|
return isAdmin;
|
||||||
|
@ -1,144 +1,144 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom/client';
|
import * as ReactDOM from 'react-dom/client';
|
||||||
import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom';
|
||||||
import Routes from './configuration/routes/RouteList';
|
import Routes from './configuration/routes/RouteList';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import Base from './pages/Base';
|
import Base from './pages/Base';
|
||||||
import About from './pages/About';
|
import About from './pages/About';
|
||||||
import Channels from './pages/Channels';
|
import Channels from './pages/Channels';
|
||||||
import ErrorPage from './pages/ErrorPage';
|
import ErrorPage from './pages/ErrorPage';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Playlist from './pages/Playlist';
|
import Playlist from './pages/Playlist';
|
||||||
import Playlists from './pages/Playlists';
|
import Playlists from './pages/Playlists';
|
||||||
import Search from './pages/Search';
|
import Search from './pages/Search';
|
||||||
import SettingsDashboard from './pages/SettingsDashboard';
|
import SettingsDashboard from './pages/SettingsDashboard';
|
||||||
import Video from './pages/Video';
|
import Video from './pages/Video';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import SettingsActions from './pages/SettingsActions';
|
import SettingsActions from './pages/SettingsActions';
|
||||||
import SettingsApplication from './pages/SettingsApplication';
|
import SettingsApplication from './pages/SettingsApplication';
|
||||||
import SettingsScheduling from './pages/SettingsScheduling';
|
import SettingsScheduling from './pages/SettingsScheduling';
|
||||||
import SettingsUser from './pages/SettingsUser';
|
import SettingsUser from './pages/SettingsUser';
|
||||||
import loadUserMeConfig from './api/loader/loadUserConfig';
|
import loadUserMeConfig from './api/loader/loadUserConfig';
|
||||||
import loadAuth from './api/loader/loadAuth';
|
import loadAuth from './api/loader/loadAuth';
|
||||||
import ChannelBase from './pages/ChannelBase';
|
import ChannelBase from './pages/ChannelBase';
|
||||||
import ChannelVideo from './pages/ChannelVideo';
|
import ChannelVideo from './pages/ChannelVideo';
|
||||||
import ChannelPlaylist from './pages/ChannelPlaylist';
|
import ChannelPlaylist from './pages/ChannelPlaylist';
|
||||||
import ChannelAbout from './pages/ChannelAbout';
|
import ChannelAbout from './pages/ChannelAbout';
|
||||||
import Download from './pages/Download';
|
import Download from './pages/Download';
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
path: Routes.Home,
|
path: Routes.Home,
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
console.log('------------ after reload');
|
console.log('------------ after reload');
|
||||||
|
|
||||||
const auth = await loadAuth();
|
const auth = await loadAuth();
|
||||||
if (auth.status === 403) {
|
if (auth.status === 403) {
|
||||||
return redirect(Routes.Login);
|
return redirect(Routes.Login);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authData = await auth.json();
|
const authData = await auth.json();
|
||||||
|
|
||||||
const userConfig = await loadUserMeConfig();
|
const userConfig = await loadUserMeConfig();
|
||||||
|
|
||||||
return { userConfig, auth: authData };
|
return { userConfig, auth: authData };
|
||||||
},
|
},
|
||||||
element: <Base />,
|
element: <Base />,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Video(':videoId'),
|
path: Routes.Video(':videoId'),
|
||||||
element: <Video />,
|
element: <Video />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Channels,
|
path: Routes.Channels,
|
||||||
element: <Channels />,
|
element: <Channels />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Channel(':channelId'),
|
path: Routes.Channel(':channelId'),
|
||||||
element: <ChannelBase />,
|
element: <ChannelBase />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
path: Routes.ChannelVideo(':channelId'),
|
path: Routes.ChannelVideo(':channelId'),
|
||||||
element: <ChannelVideo videoType="videos" />,
|
element: <ChannelVideo videoType="videos" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.ChannelStream(':channelId'),
|
path: Routes.ChannelStream(':channelId'),
|
||||||
element: <ChannelVideo videoType="streams" />,
|
element: <ChannelVideo videoType="streams" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.ChannelShorts(':channelId'),
|
path: Routes.ChannelShorts(':channelId'),
|
||||||
element: <ChannelVideo videoType="shorts" />,
|
element: <ChannelVideo videoType="shorts" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.ChannelPlaylist(':channelId'),
|
path: Routes.ChannelPlaylist(':channelId'),
|
||||||
element: <ChannelPlaylist />,
|
element: <ChannelPlaylist />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.ChannelAbout(':channelId'),
|
path: Routes.ChannelAbout(':channelId'),
|
||||||
element: <ChannelAbout />,
|
element: <ChannelAbout />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Playlists,
|
path: Routes.Playlists,
|
||||||
element: <Playlists />,
|
element: <Playlists />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Playlist(':playlistId'),
|
path: Routes.Playlist(':playlistId'),
|
||||||
element: <Playlist />,
|
element: <Playlist />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Downloads,
|
path: Routes.Downloads,
|
||||||
element: <Download />,
|
element: <Download />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Search,
|
path: Routes.Search,
|
||||||
element: <Search />,
|
element: <Search />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.SettingsDashboard,
|
path: Routes.SettingsDashboard,
|
||||||
element: <SettingsDashboard />,
|
element: <SettingsDashboard />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.SettingsActions,
|
path: Routes.SettingsActions,
|
||||||
element: <SettingsActions />,
|
element: <SettingsActions />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.SettingsApplication,
|
path: Routes.SettingsApplication,
|
||||||
element: <SettingsApplication />,
|
element: <SettingsApplication />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.SettingsScheduling,
|
path: Routes.SettingsScheduling,
|
||||||
element: <SettingsScheduling />,
|
element: <SettingsScheduling />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.SettingsUser,
|
path: Routes.SettingsUser,
|
||||||
element: <SettingsUser />,
|
element: <SettingsUser />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.About,
|
path: Routes.About,
|
||||||
element: <About />,
|
element: <About />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.Login,
|
path: Routes.Login,
|
||||||
element: <Login />,
|
element: <Login />,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ basename: import.meta.env.BASE_URL },
|
{ basename: import.meta.env.BASE_URL },
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
@ -1,60 +1,60 @@
|
|||||||
const About = () => {
|
const About = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | About</title>
|
<title>TA | About</title>
|
||||||
|
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>About The Tube Archivist</h1>
|
<h1>About The Tube Archivist</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="about-section">
|
<div className="about-section">
|
||||||
<h2>Useful Links</h2>
|
<h2>Useful Links</h2>
|
||||||
<p>
|
<p>
|
||||||
This project is in active and constant development, take a look at the{' '}
|
This project is in active and constant development, take a look at the{' '}
|
||||||
<a href="https://github.com/tubearchivist/tubearchivist#roadmap" target="_blank">
|
<a href="https://github.com/tubearchivist/tubearchivist#roadmap" target="_blank">
|
||||||
roadmap
|
roadmap
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for a overview.
|
for a overview.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
All functionality is documented in our up-to-date{' '}
|
All functionality is documented in our up-to-date{' '}
|
||||||
<a href="https://docs.tubearchivist.com" target="_blank">
|
<a href="https://docs.tubearchivist.com" target="_blank">
|
||||||
user guide
|
user guide
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
All contributions are welcome: Open an{' '}
|
All contributions are welcome: Open an{' '}
|
||||||
<a href="https://github.com/tubearchivist/tubearchivist/issues" target="_blank">
|
<a href="https://github.com/tubearchivist/tubearchivist/issues" target="_blank">
|
||||||
issue
|
issue
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for any bugs and errors, join us on{' '}
|
for any bugs and errors, join us on{' '}
|
||||||
<a href="https://www.tubearchivist.com/discord" target="_blank">
|
<a href="https://www.tubearchivist.com/discord" target="_blank">
|
||||||
Discord
|
Discord
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
to discuss details. The{' '}
|
to discuss details. The{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md"
|
href="https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
contributing
|
contributing
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
page is a good place to get started.
|
page is a good place to get started.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="about-section">
|
<div className="about-section">
|
||||||
<h2>Donate</h2>
|
<h2>Donate</h2>
|
||||||
<p>
|
<p>
|
||||||
Here are{' '}
|
Here are{' '}
|
||||||
<a href="https://github.com/tubearchivist/tubearchivist#donate" target="_blank">
|
<a href="https://github.com/tubearchivist/tubearchivist#donate" target="_blank">
|
||||||
some links
|
some links
|
||||||
</a>
|
</a>
|
||||||
, if you want to buy the developer a coffee. Thank you for your support!
|
, if you want to buy the developer a coffee. Thank you for your support!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
|
@ -31,7 +31,7 @@ export type OutletContextType = {
|
|||||||
|
|
||||||
const Base = () => {
|
const Base = () => {
|
||||||
const { setAuth } = useAuthStore();
|
const { setAuth } = useAuthStore();
|
||||||
const { setUserConfig } = useUserConfigStore()
|
const { setUserConfig } = useUserConfigStore();
|
||||||
const { userConfig, auth } = useLoaderData() as BaseLoaderData;
|
const { userConfig, auth } = useLoaderData() as BaseLoaderData;
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -46,7 +46,7 @@ const Base = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAuth(auth);
|
setAuth(auth);
|
||||||
setUserConfig(userConfig);
|
setUserConfig(userConfig);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPageFromUrl !== currentPage) {
|
if (currentPageFromUrl !== currentPage) {
|
||||||
|
@ -1,105 +1,105 @@
|
|||||||
import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
|
import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import { ChannelType } from './Channels';
|
import { ChannelType } from './Channels';
|
||||||
import { ConfigType } from './Home';
|
import { ConfigType } from './Home';
|
||||||
import { OutletContextType } from './Base';
|
import { OutletContextType } from './Base';
|
||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import ChannelBanner from '../components/ChannelBanner';
|
import ChannelBanner from '../components/ChannelBanner';
|
||||||
import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
|
import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
|
||||||
import loadChannelById from '../api/loader/loadChannelById';
|
import loadChannelById from '../api/loader/loadChannelById';
|
||||||
import loadIsAdmin from '../functions/getIsAdmin';
|
import loadIsAdmin from '../functions/getIsAdmin';
|
||||||
|
|
||||||
type ChannelParams = {
|
type ChannelParams = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelResponseType = {
|
export type ChannelResponseType = {
|
||||||
data: ChannelType;
|
data: ChannelType;
|
||||||
config: ConfigType;
|
config: ConfigType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChannelBase = () => {
|
const ChannelBase = () => {
|
||||||
const { channelId } = useParams() as ChannelParams;
|
const { channelId } = useParams() as ChannelParams;
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
const isAdmin = loadIsAdmin();
|
const isAdmin = loadIsAdmin();
|
||||||
|
|
||||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||||
const [channelNav, setChannelNav] = useState<ChannelNavResponseType>();
|
const [channelNav, setChannelNav] = useState<ChannelNavResponseType>();
|
||||||
const [startNotification, setStartNotification] = useState(false);
|
const [startNotification, setStartNotification] = useState(false);
|
||||||
|
|
||||||
const channel = channelResponse?.data;
|
const channel = channelResponse?.data;
|
||||||
const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
|
const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const channelNavResponse = await loadChannelNav(channelId);
|
const channelNavResponse = await loadChannelNav(channelId);
|
||||||
const channelResponse = await loadChannelById(channelId);
|
const channelResponse = await loadChannelById(channelId);
|
||||||
|
|
||||||
setChannelResponse(channelResponse);
|
setChannelResponse(channelResponse);
|
||||||
setChannelNav(channelNavResponse);
|
setChannelNav(channelNavResponse);
|
||||||
})();
|
})();
|
||||||
}, [channelId]);
|
}, [channelId]);
|
||||||
|
|
||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<div className="channel-banner">
|
<div className="channel-banner">
|
||||||
<Link to={Routes.ChannelVideo(channelId)}>
|
<Link to={Routes.ChannelVideo(channelId)}>
|
||||||
<ChannelBanner channelId={channelId} channelBannerUrl={channel?.channel_banner_url} />
|
<ChannelBanner channelId={channelId} channelBannerUrl={channel?.channel_banner_url} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-box-item child-page-nav">
|
<div className="info-box-item child-page-nav">
|
||||||
<Link to={Routes.ChannelVideo(channelId)}>
|
<Link to={Routes.ChannelVideo(channelId)}>
|
||||||
<h3>Videos</h3>
|
<h3>Videos</h3>
|
||||||
</Link>
|
</Link>
|
||||||
{has_streams && (
|
{has_streams && (
|
||||||
<Link to={Routes.ChannelStream(channelId)}>
|
<Link to={Routes.ChannelStream(channelId)}>
|
||||||
<h3>Streams</h3>
|
<h3>Streams</h3>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{has_shorts && (
|
{has_shorts && (
|
||||||
<Link to={Routes.ChannelShorts(channelId)}>
|
<Link to={Routes.ChannelShorts(channelId)}>
|
||||||
<h3>Shorts</h3>
|
<h3>Shorts</h3>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{has_playlists && (
|
{has_playlists && (
|
||||||
<Link to={Routes.ChannelPlaylist(channelId)}>
|
<Link to={Routes.ChannelPlaylist(channelId)}>
|
||||||
<h3>Playlists</h3>
|
<h3>Playlists</h3>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link to={Routes.ChannelAbout(channelId)}>
|
<Link to={Routes.ChannelAbout(channelId)}>
|
||||||
<h3>About</h3>
|
<h3>About</h3>
|
||||||
</Link>
|
</Link>
|
||||||
{has_pending && isAdmin && (
|
{has_pending && isAdmin && (
|
||||||
<Link to={Routes.DownloadsByChannelId(channelId)}>
|
<Link to={Routes.DownloadsByChannelId(channelId)}>
|
||||||
<h3>Downloads</h3>
|
<h3>Downloads</h3>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Notifications
|
<Notifications
|
||||||
pageName="channel"
|
pageName="channel"
|
||||||
includeReindex={true}
|
includeReindex={true}
|
||||||
update={startNotification}
|
update={startNotification}
|
||||||
setShouldRefresh={() => setStartNotification(false)}
|
setShouldRefresh={() => setStartNotification(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Outlet
|
<Outlet
|
||||||
context={{
|
context={{
|
||||||
currentPage,
|
currentPage,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
startNotification,
|
startNotification,
|
||||||
setStartNotification,
|
setStartNotification,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChannelBase;
|
export default ChannelBase;
|
||||||
|
@ -1,107 +1,104 @@
|
|||||||
import { useOutletContext, useParams } from 'react-router-dom';
|
import { useOutletContext, useParams } from 'react-router-dom';
|
||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import PlaylistList from '../components/PlaylistList';
|
import PlaylistList from '../components/PlaylistList';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { OutletContextType } from './Base';
|
import { OutletContextType } from './Base';
|
||||||
import Pagination from '../components/Pagination';
|
import Pagination from '../components/Pagination';
|
||||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||||
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
||||||
import { PlaylistsResponseType } from './Playlists';
|
import { PlaylistsResponseType } from './Playlists';
|
||||||
import iconGridView from '/img/icon-gridview.svg';
|
import iconGridView from '/img/icon-gridview.svg';
|
||||||
import iconListView from '/img/icon-listview.svg';
|
import iconListView from '/img/icon-listview.svg';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
const ChannelPlaylist = () => {
|
const ChannelPlaylist = () => {
|
||||||
const { channelId } = useParams();
|
const { channelId } = useParams();
|
||||||
const { userConfig, setPartialConfig } = useUserConfigStore();
|
const { userConfig, setPartialConfig } = useUserConfigStore();
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
|
|
||||||
const [refreshPlaylists, setRefreshPlaylists] = useState(false);
|
const [refreshPlaylists, setRefreshPlaylists] = useState(false);
|
||||||
|
|
||||||
const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>();
|
const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>();
|
||||||
|
|
||||||
const playlistList = playlistsResponse?.data;
|
const playlistList = playlistsResponse?.data;
|
||||||
const pagination = playlistsResponse?.paginate;
|
const pagination = playlistsResponse?.paginate;
|
||||||
|
|
||||||
const view = userConfig.config.view_style_playlist;
|
const view = userConfig.config.view_style_playlist;
|
||||||
const showSubedOnly = userConfig.config.show_subed_only;
|
const showSubedOnly = userConfig.config.show_subed_only;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const playlists = await loadPlaylistList({
|
const playlists = await loadPlaylistList({
|
||||||
channel: channelId,
|
channel: channelId,
|
||||||
subscribed: showSubedOnly,
|
subscribed: showSubedOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
setPlaylistsResponse(playlists);
|
setPlaylistsResponse(playlists);
|
||||||
setRefreshPlaylists(false);
|
setRefreshPlaylists(false);
|
||||||
})();
|
})();
|
||||||
}, [channelId, refreshPlaylists, showSubedOnly, currentPage]);
|
}, [channelId, refreshPlaylists, showSubedOnly, currentPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | Channel: Playlists</title>
|
<title>TA | Channel: Playlists</title>
|
||||||
<ScrollToTopOnNavigate />
|
<ScrollToTopOnNavigate />
|
||||||
<div className='boxed-content'>
|
<div className="boxed-content">
|
||||||
<Notifications pageName="channel" includeReindex={true} />
|
<Notifications pageName="channel" includeReindex={true} />
|
||||||
|
|
||||||
<div className="view-controls">
|
<div className="view-controls">
|
||||||
<div className="toggle">
|
<div className="toggle">
|
||||||
<span>Show subscribed only:</span>
|
<span>Show subscribed only:</span>
|
||||||
<div className="toggleBox">
|
<div className="toggleBox">
|
||||||
<input
|
<input
|
||||||
checked={showSubedOnly}
|
checked={showSubedOnly}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setPartialConfig({show_subed_only: !showSubedOnly});
|
setPartialConfig({ show_subed_only: !showSubedOnly });
|
||||||
setRefreshPlaylists(true);
|
setRefreshPlaylists(true);
|
||||||
}}
|
}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
{!showSubedOnly && (
|
{!showSubedOnly && (
|
||||||
<label htmlFor="" className="ofbtn">
|
<label htmlFor="" className="ofbtn">
|
||||||
Off
|
Off
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{showSubedOnly && (
|
{showSubedOnly && (
|
||||||
<label htmlFor="" className="onbtn">
|
<label htmlFor="" className="onbtn">
|
||||||
On
|
On
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="view-icons">
|
<div className="view-icons">
|
||||||
<img
|
<img
|
||||||
src={iconGridView}
|
src={iconGridView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({view_style_playlist: 'grid'});
|
setPartialConfig({ view_style_playlist: 'grid' });
|
||||||
}}
|
}}
|
||||||
alt="grid view"
|
alt="grid view"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={iconListView}
|
src={iconListView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({view_style_playlist: 'list'});
|
setPartialConfig({ view_style_playlist: 'list' });
|
||||||
}}
|
}}
|
||||||
alt="list view"
|
alt="list view"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`boxed-content`}>
|
<div className={`boxed-content`}>
|
||||||
<div className={`playlist-list ${view}`}>
|
<div className={`playlist-list ${view}`}>
|
||||||
<PlaylistList
|
<PlaylistList playlistList={playlistList} setRefresh={setRefreshPlaylists} />
|
||||||
playlistList={playlistList}
|
</div>
|
||||||
setRefresh={setRefreshPlaylists}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
<div className="boxed-content">
|
||||||
</div>
|
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
|
||||||
|
</div>
|
||||||
<div className="boxed-content">
|
</>
|
||||||
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
|
);
|
||||||
</div>
|
};
|
||||||
</>
|
|
||||||
);
|
export default ChannelPlaylist;
|
||||||
};
|
|
||||||
|
|
||||||
export default ChannelPlaylist;
|
|
||||||
|
@ -1,193 +1,188 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import { Link, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
|
||||||
Link,
|
import { OutletContextType } from './Base';
|
||||||
useOutletContext,
|
import VideoList from '../components/VideoList';
|
||||||
useParams,
|
import Routes from '../configuration/routes/RouteList';
|
||||||
useSearchParams,
|
import Pagination from '../components/Pagination';
|
||||||
} from 'react-router-dom';
|
import Filterbar from '../components/Filterbar';
|
||||||
import { OutletContextType } from './Base';
|
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
||||||
import VideoList from '../components/VideoList';
|
import ChannelOverview from '../components/ChannelOverview';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import loadChannelById from '../api/loader/loadChannelById';
|
||||||
import Pagination from '../components/Pagination';
|
import { ChannelResponseType } from './ChannelBase';
|
||||||
import Filterbar from '../components/Filterbar';
|
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||||
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||||
import ChannelOverview from '../components/ChannelOverview';
|
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||||
import loadChannelById from '../api/loader/loadChannelById';
|
import Button from '../components/Button';
|
||||||
import { ChannelResponseType } from './ChannelBase';
|
import loadVideoListByFilter, {
|
||||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
VideoListByFilterResponseType,
|
||||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
VideoTypes,
|
||||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
} from '../api/loader/loadVideoListByPage';
|
||||||
import Button from '../components/Button';
|
import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
|
||||||
import loadVideoListByFilter, {
|
import humanFileSize from '../functions/humanFileSize';
|
||||||
VideoListByFilterResponseType,
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
VideoTypes,
|
|
||||||
} from '../api/loader/loadVideoListByPage';
|
type ChannelParams = {
|
||||||
import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
|
channelId: string;
|
||||||
import humanFileSize from '../functions/humanFileSize';
|
};
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
|
||||||
|
type ChannelVideoProps = {
|
||||||
type ChannelParams = {
|
videoType: VideoTypes;
|
||||||
channelId: string;
|
};
|
||||||
};
|
|
||||||
|
const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
|
||||||
type ChannelVideoProps = {
|
const { channelId } = useParams() as ChannelParams;
|
||||||
videoType: VideoTypes;
|
const { userConfig } = useUserConfigStore();
|
||||||
};
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
|
const videoId = searchParams.get('videoId');
|
||||||
const { channelId } = useParams() as ChannelParams;
|
|
||||||
const { userConfig } = useUserConfigStore();
|
const [refresh, setRefresh] = useState(false);
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
|
||||||
const [searchParams] = useSearchParams();
|
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||||
const videoId = searchParams.get('videoId');
|
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
||||||
|
const [videoAggsResponse, setVideoAggsResponse] = useState<ChannelAggsType>();
|
||||||
const [refresh, setRefresh] = useState(false);
|
|
||||||
|
const channel = channelResponse?.data;
|
||||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
const videoList = videoResponse?.data;
|
||||||
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
const pagination = videoResponse?.paginate;
|
||||||
const [videoAggsResponse, setVideoAggsResponse] = useState<ChannelAggsType>();
|
|
||||||
|
const hasVideos = videoResponse?.data?.length !== 0;
|
||||||
const channel = channelResponse?.data;
|
const showEmbeddedVideo = videoId !== null;
|
||||||
const videoList = videoResponse?.data;
|
|
||||||
const pagination = videoResponse?.paginate;
|
const view = userConfig.config.view_style_home;
|
||||||
|
const isGridView = view === ViewStyles.grid;
|
||||||
const hasVideos = videoResponse?.data?.length !== 0;
|
const gridView = isGridView ? `boxed-${userConfig.config.grid_items}` : '';
|
||||||
const showEmbeddedVideo = videoId !== null;
|
const gridViewGrid = isGridView ? `grid-${userConfig.config.grid_items}` : '';
|
||||||
|
|
||||||
const view = userConfig.config.view_style_home
|
useEffect(() => {
|
||||||
const isGridView = view === ViewStyles.grid;
|
(async () => {
|
||||||
const gridView = isGridView ? `boxed-${userConfig.config.grid_items}` : '';
|
const channelResponse = await loadChannelById(channelId);
|
||||||
const gridViewGrid = isGridView ? `grid-${userConfig.config.grid_items}` : '';
|
const videos = await loadVideoListByFilter({
|
||||||
|
channel: channelId,
|
||||||
useEffect(() => {
|
page: currentPage,
|
||||||
(async () => {
|
watch: userConfig.config.hide_watched ? 'unwatched' : undefined,
|
||||||
const channelResponse = await loadChannelById(channelId);
|
sort: userConfig.config.sort_by,
|
||||||
const videos = await loadVideoListByFilter({
|
order: userConfig.config.sort_order,
|
||||||
channel: channelId,
|
type: videoType,
|
||||||
page: currentPage,
|
});
|
||||||
watch: userConfig.config.hide_watched ? 'unwatched' : undefined,
|
const channelAggs = await loadChannelAggs(channelId);
|
||||||
sort: userConfig.config.sort_by,
|
|
||||||
order: userConfig.config.sort_order,
|
setChannelResponse(channelResponse);
|
||||||
type: videoType,
|
setVideoReponse(videos);
|
||||||
});
|
setVideoAggsResponse(channelAggs);
|
||||||
const channelAggs = await loadChannelAggs(channelId);
|
setRefresh(false);
|
||||||
|
})();
|
||||||
setChannelResponse(channelResponse);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
setVideoReponse(videos);
|
}, [
|
||||||
setVideoAggsResponse(channelAggs);
|
refresh,
|
||||||
setRefresh(false);
|
userConfig.config.sort_by,
|
||||||
})();
|
userConfig.config.sort_order,
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
userConfig.config.hide_watched,
|
||||||
}, [
|
currentPage,
|
||||||
refresh,
|
channelId,
|
||||||
userConfig.config.sort_by,
|
pagination?.current_page,
|
||||||
userConfig.config.sort_order,
|
videoType,
|
||||||
userConfig.config.hide_watched,
|
]);
|
||||||
currentPage,
|
|
||||||
channelId,
|
if (!channel) {
|
||||||
pagination?.current_page,
|
return (
|
||||||
videoType,
|
<div className="boxed-content">
|
||||||
]);
|
<br />
|
||||||
|
<h2>Channel {channelId} not found!</h2>
|
||||||
if (!channel) {
|
</div>
|
||||||
return (
|
);
|
||||||
<div className="boxed-content">
|
}
|
||||||
<br />
|
|
||||||
<h2>Channel {channelId} not found!</h2>
|
return (
|
||||||
</div>
|
<>
|
||||||
);
|
<title>{`TA | Channel: ${channel.channel_name}`}</title>
|
||||||
}
|
<ScrollToTopOnNavigate />
|
||||||
|
<div className="boxed-content">
|
||||||
return (
|
<div className="info-box info-box-2">
|
||||||
<>
|
<ChannelOverview
|
||||||
<title>{`TA | Channel: ${channel.channel_name}`}</title>
|
channelId={channel.channel_id}
|
||||||
<ScrollToTopOnNavigate />
|
channelname={channel.channel_name}
|
||||||
<div className="boxed-content">
|
channelSubs={channel.channel_subs}
|
||||||
<div className="info-box info-box-2">
|
channelSubscribed={channel.channel_subscribed}
|
||||||
<ChannelOverview
|
channelThumbUrl={channel.channel_thumb_url}
|
||||||
channelId={channel.channel_id}
|
showSubscribeButton={true}
|
||||||
channelname={channel.channel_name}
|
setRefresh={setRefresh}
|
||||||
channelSubs={channel.channel_subs}
|
/>
|
||||||
channelSubscribed={channel.channel_subscribed}
|
<div className="info-box-item">
|
||||||
channelThumbUrl={channel.channel_thumb_url}
|
{videoAggsResponse && (
|
||||||
showSubscribeButton={true}
|
<>
|
||||||
setRefresh={setRefresh}
|
<p>
|
||||||
/>
|
{videoAggsResponse.total_items.value} videos{' '}
|
||||||
<div className="info-box-item">
|
<span className="space-carrot">|</span>{' '}
|
||||||
{videoAggsResponse && (
|
{videoAggsResponse.total_duration.value_str} playback{' '}
|
||||||
<>
|
<span className="space-carrot">|</span> Total size{' '}
|
||||||
<p>
|
{humanFileSize(videoAggsResponse.total_size.value, true)}
|
||||||
{videoAggsResponse.total_items.value} videos{' '}
|
</p>
|
||||||
<span className="space-carrot">|</span>{' '}
|
<div className="button-box">
|
||||||
{videoAggsResponse.total_duration.value_str} playback{' '}
|
<Button
|
||||||
<span className="space-carrot">|</span> Total size{' '}
|
label="Mark as watched"
|
||||||
{humanFileSize(videoAggsResponse.total_size.value, true)}
|
id="watched-button"
|
||||||
</p>
|
type="button"
|
||||||
<div className="button-box">
|
title={`Mark all videos from ${channel.channel_name} as watched`}
|
||||||
<Button
|
onClick={async () => {
|
||||||
label="Mark as watched"
|
await updateWatchedState({
|
||||||
id="watched-button"
|
id: channel.channel_id,
|
||||||
type="button"
|
is_watched: true,
|
||||||
title={`Mark all videos from ${channel.channel_name} as watched`}
|
});
|
||||||
onClick={async () => {
|
|
||||||
await updateWatchedState({
|
setRefresh(true);
|
||||||
id: channel.channel_id,
|
}}
|
||||||
is_watched: true,
|
/>{' '}
|
||||||
});
|
<Button
|
||||||
|
label="Mark as unwatched"
|
||||||
setRefresh(true);
|
id="unwatched-button"
|
||||||
}}
|
type="button"
|
||||||
/>{' '}
|
title={`Mark all videos from ${channel.channel_name} as unwatched`}
|
||||||
<Button
|
onClick={async () => {
|
||||||
label="Mark as unwatched"
|
await updateWatchedState({
|
||||||
id="unwatched-button"
|
id: channel.channel_id,
|
||||||
type="button"
|
is_watched: false,
|
||||||
title={`Mark all videos from ${channel.channel_name} as unwatched`}
|
});
|
||||||
onClick={async () => {
|
|
||||||
await updateWatchedState({
|
setRefresh(true);
|
||||||
id: channel.channel_id,
|
}}
|
||||||
is_watched: false,
|
/>
|
||||||
});
|
</div>
|
||||||
|
</>
|
||||||
setRefresh(true);
|
)}
|
||||||
}}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className={`boxed-content ${gridView}`}>
|
||||||
)}
|
<Filterbar
|
||||||
</div>
|
hideToggleText={'Hide watched videos:'}
|
||||||
</div>
|
viewStyleName={ViewStyleNames.home}
|
||||||
</div>
|
setRefresh={setRefresh}
|
||||||
<div className={`boxed-content ${gridView}`}>
|
/>
|
||||||
<Filterbar
|
</div>
|
||||||
hideToggleText={'Hide watched videos:'}
|
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
||||||
viewStyleName={ViewStyleNames.home}
|
<div className={`boxed-content ${gridView}`}>
|
||||||
setRefresh={setRefresh}
|
<div className={`video-list ${view} ${gridViewGrid}`}>
|
||||||
/>
|
{!hasVideos && (
|
||||||
</div>
|
<>
|
||||||
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
<h2>No videos found...</h2>
|
||||||
<div className={`boxed-content ${gridView}`}>
|
<p>
|
||||||
<div className={`video-list ${view} ${gridViewGrid}`}>
|
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the scan
|
||||||
{!hasVideos && (
|
and download tasks.
|
||||||
<>
|
</p>
|
||||||
<h2>No videos found...</h2>
|
</>
|
||||||
<p>
|
)}
|
||||||
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the scan
|
|
||||||
and download tasks.
|
<VideoList videoList={videoList} viewLayout={view} refreshVideoList={setRefresh} />
|
||||||
</p>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
{pagination && (
|
||||||
|
<div className="boxed-content">
|
||||||
<VideoList videoList={videoList} viewLayout={view} refreshVideoList={setRefresh} />
|
<Pagination pagination={pagination} setPage={setCurrentPage} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{pagination && (
|
</>
|
||||||
<div className="boxed-content">
|
);
|
||||||
<Pagination pagination={pagination} setPage={setCurrentPage} />
|
};
|
||||||
</div>
|
|
||||||
)}
|
export default ChannelVideo;
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChannelVideo;
|
|
||||||
|
@ -1,189 +1,190 @@
|
|||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
import loadChannelList from '../api/loader/loadChannelList';
|
import loadChannelList from '../api/loader/loadChannelList';
|
||||||
import iconGridView from '/img/icon-gridview.svg';
|
import iconGridView from '/img/icon-gridview.svg';
|
||||||
import iconListView from '/img/icon-listview.svg';
|
import iconListView from '/img/icon-listview.svg';
|
||||||
import iconAdd from '/img/icon-add.svg';
|
import iconAdd from '/img/icon-add.svg';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Pagination, { PaginationType } from '../components/Pagination';
|
import Pagination, { PaginationType } from '../components/Pagination';
|
||||||
import { ConfigType } from './Home';
|
import { ConfigType } from './Home';
|
||||||
import { OutletContextType } from './Base';
|
import { OutletContextType } from './Base';
|
||||||
import ChannelList from '../components/ChannelList';
|
import ChannelList from '../components/ChannelList';
|
||||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
|
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
|
||||||
import loadIsAdmin from '../functions/getIsAdmin';
|
import loadIsAdmin from '../functions/getIsAdmin';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
type ChannelOverwritesType = {
|
type ChannelOverwritesType = {
|
||||||
download_format?: string;
|
download_format?: string;
|
||||||
autodelete_days?: number;
|
autodelete_days?: number;
|
||||||
index_playlists?: boolean;
|
index_playlists?: boolean;
|
||||||
integrate_sponsorblock?: boolean;
|
integrate_sponsorblock?: boolean;
|
||||||
subscriptions_channel_size?: number;
|
subscriptions_channel_size?: number;
|
||||||
subscriptions_live_channel_size?: number;
|
subscriptions_live_channel_size?: number;
|
||||||
subscriptions_shorts_channel_size?: number;
|
subscriptions_shorts_channel_size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelType = {
|
export type ChannelType = {
|
||||||
channel_active: boolean;
|
channel_active: boolean;
|
||||||
channel_banner_url: string;
|
channel_banner_url: string;
|
||||||
channel_description: string;
|
channel_description: string;
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
channel_last_refresh: string;
|
channel_last_refresh: string;
|
||||||
channel_name: string;
|
channel_name: string;
|
||||||
channel_overwrites?: ChannelOverwritesType;
|
channel_overwrites?: ChannelOverwritesType;
|
||||||
channel_subs: number;
|
channel_subs: number;
|
||||||
channel_subscribed: boolean;
|
channel_subscribed: boolean;
|
||||||
channel_tags: string[];
|
channel_tags: string[];
|
||||||
channel_thumb_url: string;
|
channel_thumb_url: string;
|
||||||
channel_tvart_url: string;
|
channel_tvart_url: string;
|
||||||
channel_views: number;
|
channel_views: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChannelsListResponse = {
|
type ChannelsListResponse = {
|
||||||
data: ChannelType[];
|
data: ChannelType[];
|
||||||
paginate: PaginationType;
|
paginate: PaginationType;
|
||||||
config?: ConfigType;
|
config?: ConfigType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Channels = () => {
|
const Channels = () => {
|
||||||
const { userConfig, setPartialConfig } = useUserConfigStore();
|
const { userConfig, setPartialConfig } = useUserConfigStore();
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
const isAdmin = loadIsAdmin();
|
const isAdmin = loadIsAdmin();
|
||||||
|
|
||||||
const [channelListResponse, setChannelListResponse] = useState<ChannelsListResponse>();
|
const [channelListResponse, setChannelListResponse] = useState<ChannelsListResponse>();
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refresh, setRefresh] = useState(false);
|
||||||
const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
|
const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
|
||||||
|
|
||||||
const channels = channelListResponse?.data;
|
const channels = channelListResponse?.data;
|
||||||
const pagination = channelListResponse?.paginate;
|
const pagination = channelListResponse?.paginate;
|
||||||
const channelCount = pagination?.total_hits;
|
const channelCount = pagination?.total_hits;
|
||||||
const hasChannels = channels?.length !== 0;
|
const hasChannels = channels?.length !== 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const channelListResponse = await loadChannelList(currentPage, userConfig.config.show_subed_only);
|
const channelListResponse = await loadChannelList(
|
||||||
setChannelListResponse(channelListResponse);
|
currentPage,
|
||||||
})();
|
userConfig.config.show_subed_only,
|
||||||
}, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
|
);
|
||||||
|
setChannelListResponse(channelListResponse);
|
||||||
return (
|
})();
|
||||||
<>
|
}, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
|
||||||
<title>TA | Channels</title>
|
|
||||||
<ScrollToTopOnNavigate />
|
return (
|
||||||
<div className="boxed-content">
|
<>
|
||||||
<div className="title-split">
|
<title>TA | Channels</title>
|
||||||
<div className="title-bar">
|
<ScrollToTopOnNavigate />
|
||||||
<h1>Channels</h1>
|
<div className="boxed-content">
|
||||||
</div>
|
<div className="title-split">
|
||||||
{isAdmin && (
|
<div className="title-bar">
|
||||||
<div className="title-split-form">
|
<h1>Channels</h1>
|
||||||
<img
|
</div>
|
||||||
id="animate-icon"
|
{isAdmin && (
|
||||||
onClick={() => {
|
<div className="title-split-form">
|
||||||
setShowAddForm(!showAddForm);
|
<img
|
||||||
}}
|
id="animate-icon"
|
||||||
src={iconAdd}
|
onClick={() => {
|
||||||
alt="add-icon"
|
setShowAddForm(!showAddForm);
|
||||||
title="Subscribe to Channels"
|
}}
|
||||||
/>
|
src={iconAdd}
|
||||||
{showAddForm && (
|
alt="add-icon"
|
||||||
<div className="show-form">
|
title="Subscribe to Channels"
|
||||||
<div>
|
/>
|
||||||
<label>Subscribe to channels:</label>
|
{showAddForm && (
|
||||||
<textarea
|
<div className="show-form">
|
||||||
value={channelsToSubscribeTo}
|
<div>
|
||||||
onChange={e => {
|
<label>Subscribe to channels:</label>
|
||||||
setChannelsToSubscribeTo(e.currentTarget.value);
|
<textarea
|
||||||
}}
|
value={channelsToSubscribeTo}
|
||||||
rows={3}
|
onChange={e => {
|
||||||
placeholder="Input channel ID, URL or Video of a channel"
|
setChannelsToSubscribeTo(e.currentTarget.value);
|
||||||
/>
|
}}
|
||||||
</div>
|
rows={3}
|
||||||
|
placeholder="Input channel ID, URL or Video of a channel"
|
||||||
<Button
|
/>
|
||||||
label="Subscribe"
|
</div>
|
||||||
type="submit"
|
|
||||||
onClick={async () => {
|
<Button
|
||||||
await updateChannelSubscription(channelsToSubscribeTo, true);
|
label="Subscribe"
|
||||||
|
type="submit"
|
||||||
setRefresh(true);
|
onClick={async () => {
|
||||||
}}
|
await updateChannelSubscription(channelsToSubscribeTo, true);
|
||||||
/>
|
|
||||||
</div>
|
setRefresh(true);
|
||||||
)}
|
}}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
<Notifications pageName="all" />
|
)}
|
||||||
|
</div>
|
||||||
<div className="view-controls">
|
|
||||||
<div className="toggle">
|
<Notifications pageName="all" />
|
||||||
<span>Show subscribed only:</span>
|
|
||||||
<div className="toggleBox">
|
<div className="view-controls">
|
||||||
<input
|
<div className="toggle">
|
||||||
id="show_subed_only"
|
<span>Show subscribed only:</span>
|
||||||
onChange={async () => {
|
<div className="toggleBox">
|
||||||
setPartialConfig({show_subed_only: !userConfig.config.show_subed_only});
|
<input
|
||||||
setRefresh(true);
|
id="show_subed_only"
|
||||||
}}
|
onChange={async () => {
|
||||||
type="checkbox"
|
setPartialConfig({ show_subed_only: !userConfig.config.show_subed_only });
|
||||||
checked={userConfig.config.show_subed_only}
|
setRefresh(true);
|
||||||
/>
|
}}
|
||||||
{!userConfig.config.show_subed_only && (
|
type="checkbox"
|
||||||
<label htmlFor="" className="ofbtn">
|
checked={userConfig.config.show_subed_only}
|
||||||
Off
|
/>
|
||||||
</label>
|
{!userConfig.config.show_subed_only && (
|
||||||
)}
|
<label htmlFor="" className="ofbtn">
|
||||||
{userConfig.config.show_subed_only && (
|
Off
|
||||||
<label htmlFor="" className="onbtn">
|
</label>
|
||||||
On
|
)}
|
||||||
</label>
|
{userConfig.config.show_subed_only && (
|
||||||
)}
|
<label htmlFor="" className="onbtn">
|
||||||
</div>
|
On
|
||||||
</div>
|
</label>
|
||||||
<div className="view-icons">
|
)}
|
||||||
<img
|
</div>
|
||||||
src={iconGridView}
|
</div>
|
||||||
onClick={() => {
|
<div className="view-icons">
|
||||||
setPartialConfig({view_style_channel: 'grid'});
|
<img
|
||||||
}}
|
src={iconGridView}
|
||||||
data-origin="channel"
|
onClick={() => {
|
||||||
data-value="grid"
|
setPartialConfig({ view_style_channel: 'grid' });
|
||||||
alt="grid view"
|
}}
|
||||||
/>
|
data-origin="channel"
|
||||||
<img
|
data-value="grid"
|
||||||
src={iconListView}
|
alt="grid view"
|
||||||
onClick={() => {
|
/>
|
||||||
setPartialConfig({view_style_channel: 'list'});
|
<img
|
||||||
}}
|
src={iconListView}
|
||||||
data-origin="channel"
|
onClick={() => {
|
||||||
data-value="list"
|
setPartialConfig({ view_style_channel: 'list' });
|
||||||
alt="list view"
|
}}
|
||||||
/>
|
data-origin="channel"
|
||||||
</div>
|
data-value="list"
|
||||||
</div>
|
alt="list view"
|
||||||
{hasChannels && <h2>Total channels: {channelCount}</h2>}
|
/>
|
||||||
|
</div>
|
||||||
<div className={`channel-list ${userConfig.config.view_style_channel}`}>
|
</div>
|
||||||
{!hasChannels && <h2>No channels found...</h2>}
|
{hasChannels && <h2>Total channels: {channelCount}</h2>}
|
||||||
|
|
||||||
{hasChannels && (
|
<div className={`channel-list ${userConfig.config.view_style_channel}`}>
|
||||||
<ChannelList channelList={channels} refreshChannelList={setRefresh} />
|
{!hasChannels && <h2>No channels found...</h2>}
|
||||||
)}
|
|
||||||
</div>
|
{hasChannels && <ChannelList channelList={channels} refreshChannelList={setRefresh} />}
|
||||||
|
</div>
|
||||||
{pagination && (
|
|
||||||
<div className="boxed-content">
|
{pagination && (
|
||||||
<Pagination pagination={pagination} setPage={setCurrentPage} />
|
<div className="boxed-content">
|
||||||
</div>
|
<Pagination pagination={pagination} setPage={setCurrentPage} />
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
export default Channels;
|
|
||||||
|
export default Channels;
|
||||||
|
@ -206,7 +206,7 @@ const Download = () => {
|
|||||||
<input
|
<input
|
||||||
id="showIgnored"
|
id="showIgnored"
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setPartialConfig({show_ignored_only: !showIgnored});
|
setPartialConfig({ show_ignored_only: !showIgnored });
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -262,7 +262,7 @@ const Download = () => {
|
|||||||
<img
|
<img
|
||||||
src={iconAdd}
|
src={iconAdd}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({grid_items: gridItems + 1});
|
setPartialConfig({ grid_items: gridItems + 1 });
|
||||||
}}
|
}}
|
||||||
alt="grid plus row"
|
alt="grid plus row"
|
||||||
/>
|
/>
|
||||||
@ -271,7 +271,7 @@ const Download = () => {
|
|||||||
<img
|
<img
|
||||||
src={iconSubstract}
|
src={iconSubstract}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({grid_items: gridItems - 1});
|
setPartialConfig({ grid_items: gridItems - 1 });
|
||||||
}}
|
}}
|
||||||
alt="grid minus row"
|
alt="grid minus row"
|
||||||
/>
|
/>
|
||||||
@ -282,14 +282,14 @@ const Download = () => {
|
|||||||
<img
|
<img
|
||||||
src={iconGridView}
|
src={iconGridView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({view_style_downloads: 'grid'});
|
setPartialConfig({ view_style_downloads: 'grid' });
|
||||||
}}
|
}}
|
||||||
alt="grid view"
|
alt="grid view"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={iconListView}
|
src={iconListView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({view_style_downloads: 'list'});
|
setPartialConfig({ view_style_downloads: 'list' });
|
||||||
}}
|
}}
|
||||||
alt="list view"
|
alt="list view"
|
||||||
/>
|
/>
|
||||||
@ -313,10 +313,7 @@ const Download = () => {
|
|||||||
downloadList?.map(download => {
|
downloadList?.map(download => {
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${download.channel_id}_${download.timestamp}`}>
|
<Fragment key={`${download.channel_id}_${download.timestamp}`}>
|
||||||
<DownloadListItem
|
<DownloadListItem download={download} setRefresh={setRefresh} />
|
||||||
download={download}
|
|
||||||
setRefresh={setRefresh}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,33 +1,32 @@
|
|||||||
import { useRouteError } from 'react-router-dom';
|
import { useRouteError } from 'react-router-dom';
|
||||||
import importColours from '../configuration/colours/getColours';
|
import importColours from '../configuration/colours/getColours';
|
||||||
|
|
||||||
|
// This is not always the correct response
|
||||||
// This is not always the correct response
|
type ErrorType = {
|
||||||
type ErrorType = {
|
statusText: string;
|
||||||
statusText: string;
|
message: string;
|
||||||
message: string;
|
};
|
||||||
};
|
|
||||||
|
const ErrorPage = () => {
|
||||||
const ErrorPage = () => {
|
const error = useRouteError() as ErrorType;
|
||||||
const error = useRouteError() as ErrorType;
|
importColours();
|
||||||
importColours();
|
|
||||||
|
console.error('ErrorPage', error);
|
||||||
console.error('ErrorPage', error);
|
|
||||||
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
<title>TA | Oops!</title>
|
||||||
<title>TA | Oops!</title>
|
|
||||||
|
<div id="error-page" style={{ margin: '10%' }}>
|
||||||
<div id="error-page" style={{ margin: '10%' }}>
|
<h1>Oops!</h1>
|
||||||
<h1>Oops!</h1>
|
<p>Sorry, an unexpected error has occurred.</p>
|
||||||
<p>Sorry, an unexpected error has occurred.</p>
|
<p>
|
||||||
<p>
|
<i>{error?.statusText}</i>
|
||||||
<i>{error?.statusText}</i>
|
<i>{error?.message}</i>
|
||||||
<i>{error?.message}</i>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
export default ErrorPage;
|
||||||
export default ErrorPage;
|
|
||||||
|
@ -1,232 +1,232 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import Pagination from '../components/Pagination';
|
import Pagination from '../components/Pagination';
|
||||||
import loadVideoListByFilter, {
|
import loadVideoListByFilter, {
|
||||||
VideoListByFilterResponseType,
|
VideoListByFilterResponseType,
|
||||||
} from '../api/loader/loadVideoListByPage';
|
} from '../api/loader/loadVideoListByPage';
|
||||||
import VideoList from '../components/VideoList';
|
import VideoList from '../components/VideoList';
|
||||||
import { ChannelType } from './Channels';
|
import { ChannelType } from './Channels';
|
||||||
import { OutletContextType } from './Base';
|
import { OutletContextType } from './Base';
|
||||||
import Filterbar from '../components/Filterbar';
|
import Filterbar from '../components/Filterbar';
|
||||||
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
||||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||||
import { SponsorBlockType } from './Video';
|
import { SponsorBlockType } from './Video';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
export type PlayerType = {
|
export type PlayerType = {
|
||||||
watched: boolean;
|
watched: boolean;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
position: number;
|
position: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatsType = {
|
export type StatsType = {
|
||||||
view_count: number;
|
view_count: number;
|
||||||
like_count: number;
|
like_count: number;
|
||||||
dislike_count: number;
|
dislike_count: number;
|
||||||
average_rating: number;
|
average_rating: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StreamType = {
|
export type StreamType = {
|
||||||
type: string;
|
type: string;
|
||||||
index: number;
|
index: number;
|
||||||
codec: string;
|
codec: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
bitrate: number;
|
bitrate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Subtitles = {
|
export type Subtitles = {
|
||||||
ext: string;
|
ext: string;
|
||||||
url: string;
|
url: string;
|
||||||
name: string;
|
name: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
source: string;
|
source: string;
|
||||||
media_url: string;
|
media_url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoType = {
|
export type VideoType = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
category: string[];
|
category: string[];
|
||||||
channel: ChannelType;
|
channel: ChannelType;
|
||||||
date_downloaded: number;
|
date_downloaded: number;
|
||||||
description: string;
|
description: string;
|
||||||
comment_count?: number;
|
comment_count?: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
media_url: string;
|
media_url: string;
|
||||||
player: PlayerType;
|
player: PlayerType;
|
||||||
published: string;
|
published: string;
|
||||||
sponsorblock?: SponsorBlockType;
|
sponsorblock?: SponsorBlockType;
|
||||||
playlist?: string[];
|
playlist?: string[];
|
||||||
stats: StatsType;
|
stats: StatsType;
|
||||||
streams: StreamType[];
|
streams: StreamType[];
|
||||||
subtitles: Subtitles[];
|
subtitles: Subtitles[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
title: string;
|
title: string;
|
||||||
vid_last_refresh: string;
|
vid_last_refresh: string;
|
||||||
vid_thumb_base64: boolean;
|
vid_thumb_base64: boolean;
|
||||||
vid_thumb_url: string;
|
vid_thumb_url: string;
|
||||||
vid_type: string;
|
vid_type: string;
|
||||||
youtube_id: string;
|
youtube_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadsType = {
|
export type DownloadsType = {
|
||||||
limit_speed: boolean;
|
limit_speed: boolean;
|
||||||
sleep_interval: number;
|
sleep_interval: number;
|
||||||
autodelete_days: boolean;
|
autodelete_days: boolean;
|
||||||
format: boolean;
|
format: boolean;
|
||||||
format_sort: boolean;
|
format_sort: boolean;
|
||||||
add_metadata: boolean;
|
add_metadata: boolean;
|
||||||
add_thumbnail: boolean;
|
add_thumbnail: boolean;
|
||||||
subtitle: boolean;
|
subtitle: boolean;
|
||||||
subtitle_source: boolean;
|
subtitle_source: boolean;
|
||||||
subtitle_index: boolean;
|
subtitle_index: boolean;
|
||||||
comment_max: boolean;
|
comment_max: boolean;
|
||||||
comment_sort: string;
|
comment_sort: string;
|
||||||
cookie_import: boolean;
|
cookie_import: boolean;
|
||||||
throttledratelimit: boolean;
|
throttledratelimit: boolean;
|
||||||
extractor_lang: boolean;
|
extractor_lang: boolean;
|
||||||
integrate_ryd: boolean;
|
integrate_ryd: boolean;
|
||||||
integrate_sponsorblock: boolean;
|
integrate_sponsorblock: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigType = {
|
export type ConfigType = {
|
||||||
enable_cast: boolean;
|
enable_cast: boolean;
|
||||||
downloads: DownloadsType;
|
downloads: DownloadsType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
|
export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
|
||||||
export type SortOrderType = 'asc' | 'desc';
|
export type SortOrderType = 'asc' | 'desc';
|
||||||
export type ViewLayoutType = 'grid' | 'list';
|
export type ViewLayoutType = 'grid' | 'list';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { userConfig } = useUserConfigStore();
|
const { userConfig } = useUserConfigStore();
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const videoId = searchParams.get('videoId');
|
const videoId = searchParams.get('videoId');
|
||||||
|
|
||||||
const userMeConfig = userConfig.config;
|
const userMeConfig = userConfig.config;
|
||||||
|
|
||||||
const [refreshVideoList, setRefreshVideoList] = useState(false);
|
const [refreshVideoList, setRefreshVideoList] = useState(false);
|
||||||
|
|
||||||
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
||||||
const [continueVideoResponse, setContinueVideoResponse] =
|
const [continueVideoResponse, setContinueVideoResponse] =
|
||||||
useState<VideoListByFilterResponseType>();
|
useState<VideoListByFilterResponseType>();
|
||||||
|
|
||||||
const videoList = videoResponse?.data;
|
const videoList = videoResponse?.data;
|
||||||
const pagination = videoResponse?.paginate;
|
const pagination = videoResponse?.paginate;
|
||||||
const continueVideos = continueVideoResponse?.data;
|
const continueVideos = continueVideoResponse?.data;
|
||||||
|
|
||||||
const hasVideos = videoResponse?.data?.length !== 0;
|
const hasVideos = videoResponse?.data?.length !== 0;
|
||||||
const showEmbeddedVideo = videoId !== null;
|
const showEmbeddedVideo = videoId !== null;
|
||||||
|
|
||||||
const isGridView = userMeConfig.view_style_home === ViewStyles.grid;
|
const isGridView = userMeConfig.view_style_home === ViewStyles.grid;
|
||||||
const gridView = isGridView ? `boxed-${userMeConfig.grid_items}` : '';
|
const gridView = isGridView ? `boxed-${userMeConfig.grid_items}` : '';
|
||||||
const gridViewGrid = isGridView ? `grid-${userMeConfig.grid_items}` : '';
|
const gridViewGrid = isGridView ? `grid-${userMeConfig.grid_items}` : '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (
|
if (
|
||||||
refreshVideoList ||
|
refreshVideoList ||
|
||||||
pagination?.current_page === undefined ||
|
pagination?.current_page === undefined ||
|
||||||
currentPage !== pagination?.current_page
|
currentPage !== pagination?.current_page
|
||||||
) {
|
) {
|
||||||
const videos = await loadVideoListByFilter({
|
const videos = await loadVideoListByFilter({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
watch: userMeConfig.hide_watched ? 'unwatched' : undefined,
|
watch: userMeConfig.hide_watched ? 'unwatched' : undefined,
|
||||||
sort: userMeConfig.sort_by,
|
sort: userMeConfig.sort_by,
|
||||||
order: userMeConfig.sort_order,
|
order: userMeConfig.sort_order,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const continueVideoResponse = await loadVideoListByFilter({ watch: 'continue' });
|
const continueVideoResponse = await loadVideoListByFilter({ watch: 'continue' });
|
||||||
setContinueVideoResponse(continueVideoResponse);
|
setContinueVideoResponse(continueVideoResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Server error on continue vids?');
|
console.log('Server error on continue vids?');
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoReponse(videos);
|
setVideoReponse(videos);
|
||||||
|
|
||||||
setRefreshVideoList(false);
|
setRefreshVideoList(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
refreshVideoList,
|
refreshVideoList,
|
||||||
userMeConfig.sort_by,
|
userMeConfig.sort_by,
|
||||||
userMeConfig.sort_order,
|
userMeConfig.sort_order,
|
||||||
userMeConfig.hide_watched,
|
userMeConfig.hide_watched,
|
||||||
currentPage,
|
currentPage,
|
||||||
pagination?.current_page
|
pagination?.current_page,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TubeArchivist</title>
|
<title>TubeArchivist</title>
|
||||||
<ScrollToTopOnNavigate />
|
<ScrollToTopOnNavigate />
|
||||||
|
|
||||||
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
||||||
|
|
||||||
<div className={`boxed-content ${gridView}`}>
|
<div className={`boxed-content ${gridView}`}>
|
||||||
{continueVideos && continueVideos.length > 0 && (
|
{continueVideos && continueVideos.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Continue Watching</h1>
|
<h1>Continue Watching</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className={`video-list ${userMeConfig.view_style_home} ${gridViewGrid}`}>
|
<div className={`video-list ${userMeConfig.view_style_home} ${gridViewGrid}`}>
|
||||||
<VideoList
|
<VideoList
|
||||||
videoList={continueVideos}
|
videoList={continueVideos}
|
||||||
viewLayout={userMeConfig.view_style_home}
|
viewLayout={userMeConfig.view_style_home}
|
||||||
refreshVideoList={setRefreshVideoList}
|
refreshVideoList={setRefreshVideoList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Recent Videos</h1>
|
<h1>Recent Videos</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Filterbar
|
<Filterbar
|
||||||
hideToggleText="Hide watched:"
|
hideToggleText="Hide watched:"
|
||||||
viewStyleName={ViewStyleNames.home}
|
viewStyleName={ViewStyleNames.home}
|
||||||
setRefresh={setRefreshVideoList}
|
setRefresh={setRefreshVideoList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`boxed-content ${gridView}`}>
|
<div className={`boxed-content ${gridView}`}>
|
||||||
<div className={`video-list ${userMeConfig.view_style_home} ${gridViewGrid}`}>
|
<div className={`video-list ${userMeConfig.view_style_home} ${gridViewGrid}`}>
|
||||||
{!hasVideos && (
|
{!hasVideos && (
|
||||||
<>
|
<>
|
||||||
<h2>No videos found...</h2>
|
<h2>No videos found...</h2>
|
||||||
<p>
|
<p>
|
||||||
If you've already added a channel or playlist, try going to the{' '}
|
If you've already added a channel or playlist, try going to the{' '}
|
||||||
<Link to={Routes.Downloads}>downloads page</Link> to start the scan and download
|
<Link to={Routes.Downloads}>downloads page</Link> to start the scan and download
|
||||||
tasks.
|
tasks.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasVideos && (
|
{hasVideos && (
|
||||||
<VideoList
|
<VideoList
|
||||||
videoList={videoList}
|
videoList={videoList}
|
||||||
viewLayout={userMeConfig.view_style_home}
|
viewLayout={userMeConfig.view_style_home}
|
||||||
refreshVideoList={setRefreshVideoList}
|
refreshVideoList={setRefreshVideoList}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pagination && (
|
{pagination && (
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<Pagination pagination={pagination} setPage={setCurrentPage} />
|
<Pagination pagination={pagination} setPage={setCurrentPage} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
@ -1,103 +1,103 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import importColours from '../configuration/colours/getColours';
|
import importColours from '../configuration/colours/getColours';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import signIn from '../api/actions/signIn';
|
import signIn from '../api/actions/signIn';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [saveLogin, setSaveLogin] = useState(false);
|
const [saveLogin, setSaveLogin] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
importColours();
|
importColours();
|
||||||
|
|
||||||
const form_error = false;
|
const form_error = false;
|
||||||
|
|
||||||
const handleSubmit = async (event: { preventDefault: () => void }) => {
|
const handleSubmit = async (event: { preventDefault: () => void }) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const loginResponse = await signIn(username, password, saveLogin);
|
const loginResponse = await signIn(username, password, saveLogin);
|
||||||
|
|
||||||
const signedIn = loginResponse.status === 200;
|
const signedIn = loginResponse.status === 200;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
navigate(Routes.Home);
|
navigate(Routes.Home);
|
||||||
} else {
|
} else {
|
||||||
navigate(Routes.Login);
|
navigate(Routes.Login);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | Welcome</title>
|
<title>TA | Welcome</title>
|
||||||
<div className="boxed-content login-page">
|
<div className="boxed-content login-page">
|
||||||
<img alt="tube-archivist-logo" />
|
<img alt="tube-archivist-logo" />
|
||||||
<h1>Tube Archivist</h1>
|
<h1>Tube Archivist</h1>
|
||||||
<h2>Your Self Hosted YouTube Media Server</h2>
|
<h2>Your Self Hosted YouTube Media Server</h2>
|
||||||
|
|
||||||
{form_error && <p className="danger-zone">Failed to login.</p>}
|
{form_error && <p className="danger-zone">Failed to login.</p>}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
id="id_username"
|
id="id_username"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
maxLength={150}
|
maxLength={150}
|
||||||
required={true}
|
required={true}
|
||||||
value={username}
|
value={username}
|
||||||
onChange={event => setUsername(event.target.value)}
|
onChange={event => setUsername(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
id="id_password"
|
id="id_password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required={true}
|
required={true}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={event => setPassword(event.target.value)}
|
onChange={event => setPassword(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
Remember me:{' '}
|
Remember me:{' '}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="remember_me"
|
name="remember_me"
|
||||||
id="id_remember_me"
|
id="id_remember_me"
|
||||||
checked={saveLogin}
|
checked={saveLogin}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setSaveLogin(!saveLogin);
|
setSaveLogin(!saveLogin);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<input type="hidden" name="next" value={Routes.Home} />
|
<input type="hidden" name="next" value={Routes.Home} />
|
||||||
<Button label="Login" type="submit" />
|
<Button label="Login" type="submit" />
|
||||||
</form>
|
</form>
|
||||||
<p className="login-links">
|
<p className="login-links">
|
||||||
<span>
|
<span>
|
||||||
<a href="https://github.com/tubearchivist/tubearchivist" target="_blank">
|
<a href="https://github.com/tubearchivist/tubearchivist" target="_blank">
|
||||||
Github
|
Github
|
||||||
</a>
|
</a>
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
<span>
|
<span>
|
||||||
<a href="https://github.com/tubearchivist/tubearchivist#donate" target="_blank">
|
<a href="https://github.com/tubearchivist/tubearchivist#donate" target="_blank">
|
||||||
Donate
|
Donate
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-colors">
|
<div className="footer-colors">
|
||||||
<div className="col-1"></div>
|
<div className="col-1"></div>
|
||||||
<div className="col-2"></div>
|
<div className="col-2"></div>
|
||||||
<div className="col-3"></div>
|
<div className="col-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
@ -1,385 +1,379 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import { Link, useNavigate, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
|
||||||
Link,
|
import loadPlaylistById from '../api/loader/loadPlaylistById';
|
||||||
useNavigate,
|
import { OutletContextType } from './Base';
|
||||||
useOutletContext,
|
import { ConfigType, VideoType, ViewLayoutType } from './Home';
|
||||||
useParams,
|
import Filterbar from '../components/Filterbar';
|
||||||
useSearchParams,
|
import { PlaylistEntryType } from './Playlists';
|
||||||
} from 'react-router-dom';
|
import loadChannelById from '../api/loader/loadChannelById';
|
||||||
import loadPlaylistById from '../api/loader/loadPlaylistById';
|
import VideoList from '../components/VideoList';
|
||||||
import { OutletContextType } from './Base';
|
import Pagination, { PaginationType } from '../components/Pagination';
|
||||||
import { ConfigType, VideoType, ViewLayoutType } from './Home';
|
import ChannelOverview from '../components/ChannelOverview';
|
||||||
import Filterbar from '../components/Filterbar';
|
import Linkify from '../components/Linkify';
|
||||||
import { PlaylistEntryType } from './Playlists';
|
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
||||||
import loadChannelById from '../api/loader/loadChannelById';
|
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
||||||
import VideoList from '../components/VideoList';
|
import deletePlaylist from '../api/actions/deletePlaylist';
|
||||||
import Pagination, { PaginationType } from '../components/Pagination';
|
import Routes from '../configuration/routes/RouteList';
|
||||||
import ChannelOverview from '../components/ChannelOverview';
|
import { ChannelResponseType } from './ChannelBase';
|
||||||
import Linkify from '../components/Linkify';
|
import formatDate from '../functions/formatDates';
|
||||||
import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
|
import queueReindex from '../api/actions/queueReindex';
|
||||||
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||||
import deletePlaylist from '../api/actions/deletePlaylist';
|
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||||
import Routes from '../configuration/routes/RouteList';
|
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||||
import { ChannelResponseType } from './ChannelBase';
|
import Button from '../components/Button';
|
||||||
import formatDate from '../functions/formatDates';
|
import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
|
||||||
import queueReindex from '../api/actions/queueReindex';
|
import loadIsAdmin from '../functions/getIsAdmin';
|
||||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
|
||||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
export type PlaylistType = {
|
||||||
import Button from '../components/Button';
|
playlist_active: boolean;
|
||||||
import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
|
playlist_channel: string;
|
||||||
import loadIsAdmin from '../functions/getIsAdmin';
|
playlist_channel_id: string;
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
playlist_description: string;
|
||||||
|
playlist_entries: PlaylistEntryType[];
|
||||||
export type PlaylistType = {
|
playlist_id: string;
|
||||||
playlist_active: boolean;
|
playlist_last_refresh: string;
|
||||||
playlist_channel: string;
|
playlist_name: string;
|
||||||
playlist_channel_id: string;
|
playlist_subscribed: boolean;
|
||||||
playlist_description: string;
|
playlist_thumbnail: string;
|
||||||
playlist_entries: PlaylistEntryType[];
|
playlist_type: string;
|
||||||
playlist_id: string;
|
_index: string;
|
||||||
playlist_last_refresh: string;
|
_score: number;
|
||||||
playlist_name: string;
|
};
|
||||||
playlist_subscribed: boolean;
|
|
||||||
playlist_thumbnail: string;
|
export type PlaylistResponseType = {
|
||||||
playlist_type: string;
|
data?: PlaylistType;
|
||||||
_index: string;
|
config?: ConfigType;
|
||||||
_score: number;
|
};
|
||||||
};
|
|
||||||
|
export type VideoResponseType = {
|
||||||
export type PlaylistResponseType = {
|
data?: VideoType[];
|
||||||
data?: PlaylistType;
|
config?: ConfigType;
|
||||||
config?: ConfigType;
|
paginate?: PaginationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoResponseType = {
|
const Playlist = () => {
|
||||||
data?: VideoType[];
|
const { playlistId } = useParams();
|
||||||
config?: ConfigType;
|
const navigate = useNavigate();
|
||||||
paginate?: PaginationType;
|
const [searchParams] = useSearchParams();
|
||||||
};
|
const videoId = searchParams.get('videoId');
|
||||||
|
|
||||||
const Playlist = () => {
|
const { userConfig } = useUserConfigStore();
|
||||||
const { playlistId } = useParams();
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
const navigate = useNavigate();
|
const isAdmin = loadIsAdmin();
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const videoId = searchParams.get('videoId');
|
const userMeConfig = userConfig.config;
|
||||||
|
|
||||||
const { userConfig } = useUserConfigStore();
|
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
|
||||||
const isAdmin = loadIsAdmin();
|
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
|
||||||
|
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||||
const userMeConfig = userConfig.config;
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
|
const [reindex, setReindex] = useState(false);
|
||||||
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
|
|
||||||
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
|
const [playlistResponse, setPlaylistResponse] = useState<PlaylistResponseType>();
|
||||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
||||||
const [reindex, setReindex] = useState(false);
|
const playlist = playlistResponse?.data;
|
||||||
|
const channel = channelResponse?.data;
|
||||||
const [playlistResponse, setPlaylistResponse] = useState<PlaylistResponseType>();
|
const videos = videoResponse?.data;
|
||||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
const pagination = videoResponse?.paginate;
|
||||||
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
|
|
||||||
|
const palylistEntries = playlistResponse?.data?.playlist_entries;
|
||||||
const playlist = playlistResponse?.data;
|
const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
|
||||||
const channel = channelResponse?.data;
|
const videoInPlaylistCount = pagination?.total_hits;
|
||||||
const videos = videoResponse?.data;
|
const showEmbeddedVideo = videoId !== null;
|
||||||
const pagination = videoResponse?.paginate;
|
|
||||||
|
const isGridView = view === ViewStyles.grid;
|
||||||
const palylistEntries = playlistResponse?.data?.playlist_entries;
|
const gridView = isGridView ? `boxed-${gridItems}` : '';
|
||||||
const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
|
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
|
||||||
const videoInPlaylistCount = pagination?.total_hits;
|
|
||||||
const showEmbeddedVideo = videoId !== null;
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
const isGridView = view === ViewStyles.grid;
|
if (
|
||||||
const gridView = isGridView ? `boxed-${gridItems}` : '';
|
refresh ||
|
||||||
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
|
pagination?.current_page === undefined ||
|
||||||
|
currentPage !== pagination?.current_page
|
||||||
useEffect(() => {
|
) {
|
||||||
(async () => {
|
const playlist = await loadPlaylistById(playlistId);
|
||||||
if (
|
const video = await loadVideoListByFilter({
|
||||||
refresh ||
|
playlist: playlistId,
|
||||||
pagination?.current_page === undefined ||
|
page: currentPage,
|
||||||
currentPage !== pagination?.current_page
|
watch: hideWatched ? 'unwatched' : undefined,
|
||||||
) {
|
sort: 'downloaded', // downloaded or published? or playlist sort order?
|
||||||
const playlist = await loadPlaylistById(playlistId);
|
});
|
||||||
const video = await loadVideoListByFilter({
|
|
||||||
playlist: playlistId,
|
const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
|
||||||
page: currentPage,
|
if (!isCustomPlaylist) {
|
||||||
watch: hideWatched ? 'unwatched' : undefined,
|
const channel = await loadChannelById(playlist.data.playlist_channel_id);
|
||||||
sort: 'downloaded', // downloaded or published? or playlist sort order?
|
|
||||||
});
|
setChannelResponse(channel);
|
||||||
|
}
|
||||||
const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
|
|
||||||
if (!isCustomPlaylist) {
|
setPlaylistResponse(playlist);
|
||||||
const channel = await loadChannelById(playlist.data.playlist_channel_id);
|
setVideoResponse(video);
|
||||||
|
setRefresh(false);
|
||||||
setChannelResponse(channel);
|
}
|
||||||
}
|
})();
|
||||||
|
// Do not add hideWatched this will not work as expected!
|
||||||
setPlaylistResponse(playlist);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
setVideoResponse(video);
|
}, [playlistId, refresh, currentPage, pagination?.current_page]);
|
||||||
setRefresh(false);
|
|
||||||
}
|
if (!playlistId || !playlist) {
|
||||||
})();
|
return `Playlist ${playlistId} not found!`;
|
||||||
// Do not add hideWatched this will not work as expected!
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [playlistId, refresh, currentPage, pagination?.current_page]);
|
const isCustomPlaylist = playlist.playlist_type === 'custom';
|
||||||
|
|
||||||
if (!playlistId || !playlist) {
|
return (
|
||||||
return `Playlist ${playlistId} not found!`;
|
<>
|
||||||
}
|
<title>{`TA | Playlist: ${playlist.playlist_name}`}</title>
|
||||||
|
<ScrollToTopOnNavigate />
|
||||||
const isCustomPlaylist = playlist.playlist_type === 'custom';
|
<div className="boxed-content">
|
||||||
|
<div className="title-bar">
|
||||||
return (
|
<h1>{playlist.playlist_name}</h1>
|
||||||
<>
|
</div>
|
||||||
<title>{`TA | Playlist: ${playlist.playlist_name}`}</title>
|
<div className="info-box info-box-3">
|
||||||
<ScrollToTopOnNavigate />
|
{!isCustomPlaylist && channel && (
|
||||||
<div className="boxed-content">
|
<ChannelOverview
|
||||||
<div className="title-bar">
|
channelId={channel?.channel_id}
|
||||||
<h1>{playlist.playlist_name}</h1>
|
channelname={channel?.channel_name}
|
||||||
</div>
|
channelSubs={channel?.channel_subs}
|
||||||
<div className="info-box info-box-3">
|
channelSubscribed={channel?.channel_subscribed}
|
||||||
{!isCustomPlaylist && channel && (
|
channelThumbUrl={channel.channel_thumb_url}
|
||||||
<ChannelOverview
|
setRefresh={setRefresh}
|
||||||
channelId={channel?.channel_id}
|
/>
|
||||||
channelname={channel?.channel_name}
|
)}
|
||||||
channelSubs={channel?.channel_subs}
|
|
||||||
channelSubscribed={channel?.channel_subscribed}
|
<div className="info-box-item">
|
||||||
channelThumbUrl={channel.channel_thumb_url}
|
<div>
|
||||||
setRefresh={setRefresh}
|
<p>Last refreshed: {formatDate(playlist.playlist_last_refresh)}</p>
|
||||||
/>
|
{!isCustomPlaylist && (
|
||||||
)}
|
<>
|
||||||
|
<p>
|
||||||
<div className="info-box-item">
|
Playlist:
|
||||||
<div>
|
{playlist.playlist_subscribed && (
|
||||||
<p>Last refreshed: {formatDate(playlist.playlist_last_refresh)}</p>
|
<>
|
||||||
{!isCustomPlaylist && (
|
{isAdmin && (
|
||||||
<>
|
<Button
|
||||||
<p>
|
label="Unsubscribe"
|
||||||
Playlist:
|
className="unsubscribe"
|
||||||
{playlist.playlist_subscribed && (
|
type="button"
|
||||||
<>
|
title={`Unsubscribe from ${playlist.playlist_name}`}
|
||||||
{isAdmin && (
|
onClick={async () => {
|
||||||
<Button
|
await updatePlaylistSubscription(playlistId, false);
|
||||||
label="Unsubscribe"
|
|
||||||
className="unsubscribe"
|
setRefresh(true);
|
||||||
type="button"
|
}}
|
||||||
title={`Unsubscribe from ${playlist.playlist_name}`}
|
/>
|
||||||
onClick={async () => {
|
)}
|
||||||
await updatePlaylistSubscription(playlistId, false);
|
</>
|
||||||
|
)}{' '}
|
||||||
setRefresh(true);
|
{!playlist.playlist_subscribed && (
|
||||||
}}
|
<Button
|
||||||
/>
|
label="Subscribe"
|
||||||
)}
|
type="button"
|
||||||
</>
|
title={`Subscribe to ${playlist.playlist_name}`}
|
||||||
)}{' '}
|
onClick={async () => {
|
||||||
{!playlist.playlist_subscribed && (
|
await updatePlaylistSubscription(playlistId, true);
|
||||||
<Button
|
|
||||||
label="Subscribe"
|
setRefresh(true);
|
||||||
type="button"
|
}}
|
||||||
title={`Subscribe to ${playlist.playlist_name}`}
|
/>
|
||||||
onClick={async () => {
|
)}
|
||||||
await updatePlaylistSubscription(playlistId, true);
|
</p>
|
||||||
|
{playlist.playlist_active && (
|
||||||
setRefresh(true);
|
<p>
|
||||||
}}
|
Youtube:{' '}
|
||||||
/>
|
<a
|
||||||
)}
|
href={`https://www.youtube.com/playlist?list=${playlist.playlist_id}`}
|
||||||
</p>
|
target="_blank"
|
||||||
{playlist.playlist_active && (
|
>
|
||||||
<p>
|
Active
|
||||||
Youtube:{' '}
|
</a>
|
||||||
<a
|
</p>
|
||||||
href={`https://www.youtube.com/playlist?list=${playlist.playlist_id}`}
|
)}
|
||||||
target="_blank"
|
{!playlist.playlist_active && <p>Youtube: Deactivated</p>}
|
||||||
>
|
</>
|
||||||
Active
|
)}
|
||||||
</a>
|
|
||||||
</p>
|
{!showDeleteConfirm && (
|
||||||
)}
|
<Button
|
||||||
{!playlist.playlist_active && <p>Youtube: Deactivated</p>}
|
label="Delete Playlist"
|
||||||
</>
|
id="delete-item"
|
||||||
)}
|
onClick={() => setShowDeleteConfirm(!showDeleteConfirm)}
|
||||||
|
/>
|
||||||
{!showDeleteConfirm && (
|
)}
|
||||||
<Button
|
|
||||||
label="Delete Playlist"
|
{showDeleteConfirm && (
|
||||||
id="delete-item"
|
<div className="delete-confirm" id="delete-button">
|
||||||
onClick={() => setShowDeleteConfirm(!showDeleteConfirm)}
|
<span>Delete {playlist.playlist_name}?</span>
|
||||||
/>
|
|
||||||
)}
|
<Button
|
||||||
|
label="Delete metadata"
|
||||||
{showDeleteConfirm && (
|
onClick={async () => {
|
||||||
<div className="delete-confirm" id="delete-button">
|
await deletePlaylist(playlistId, false);
|
||||||
<span>Delete {playlist.playlist_name}?</span>
|
navigate(Routes.Playlists);
|
||||||
|
}}
|
||||||
<Button
|
/>
|
||||||
label="Delete metadata"
|
|
||||||
onClick={async () => {
|
<Button
|
||||||
await deletePlaylist(playlistId, false);
|
label="Delete all"
|
||||||
navigate(Routes.Playlists);
|
className="danger-button"
|
||||||
}}
|
onClick={async () => {
|
||||||
/>
|
await deletePlaylist(playlistId, true);
|
||||||
|
navigate(Routes.Playlists);
|
||||||
<Button
|
}}
|
||||||
label="Delete all"
|
/>
|
||||||
className="danger-button"
|
|
||||||
onClick={async () => {
|
<br />
|
||||||
await deletePlaylist(playlistId, true);
|
<Button label="Cancel" onClick={() => setShowDeleteConfirm(!showDeleteConfirm)} />
|
||||||
navigate(Routes.Playlists);
|
</div>
|
||||||
}}
|
)}
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
<br />
|
<div className="info-box-item">
|
||||||
<Button label="Cancel" onClick={() => setShowDeleteConfirm(!showDeleteConfirm)} />
|
<div>
|
||||||
</div>
|
{videoArchivedCount > 0 && (
|
||||||
)}
|
<>
|
||||||
</div>
|
<p>
|
||||||
</div>
|
Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
|
||||||
<div className="info-box-item">
|
</p>
|
||||||
<div>
|
<div id="watched-button" className="button-box">
|
||||||
{videoArchivedCount > 0 && (
|
<Button
|
||||||
<>
|
label="Mark as watched"
|
||||||
<p>
|
title={`Mark all videos from ${playlist.playlist_name} as watched`}
|
||||||
Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
|
type="button"
|
||||||
</p>
|
onClick={async () => {
|
||||||
<div id="watched-button" className="button-box">
|
await updateWatchedState({
|
||||||
<Button
|
id: playlistId,
|
||||||
label="Mark as watched"
|
is_watched: true,
|
||||||
title={`Mark all videos from ${playlist.playlist_name} as watched`}
|
});
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
setRefresh(true);
|
||||||
await updateWatchedState({
|
}}
|
||||||
id: playlistId,
|
/>{' '}
|
||||||
is_watched: true,
|
<Button
|
||||||
});
|
label="Mark as unwatched"
|
||||||
|
title={`Mark all videos from ${playlist.playlist_name} as unwatched`}
|
||||||
setRefresh(true);
|
type="button"
|
||||||
}}
|
onClick={async () => {
|
||||||
/>{' '}
|
await updateWatchedState({
|
||||||
<Button
|
id: playlistId,
|
||||||
label="Mark as unwatched"
|
is_watched: false,
|
||||||
title={`Mark all videos from ${playlist.playlist_name} as unwatched`}
|
});
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
setRefresh(true);
|
||||||
await updateWatchedState({
|
}}
|
||||||
id: playlistId,
|
/>
|
||||||
is_watched: false,
|
</div>
|
||||||
});
|
</>
|
||||||
|
)}
|
||||||
setRefresh(true);
|
|
||||||
}}
|
{reindex && <p>Reindex scheduled</p>}
|
||||||
/>
|
{!reindex && (
|
||||||
</div>
|
<div id="reindex-button" className="button-box">
|
||||||
</>
|
{!isCustomPlaylist && (
|
||||||
)}
|
<Button
|
||||||
|
label="Reindex"
|
||||||
{reindex && <p>Reindex scheduled</p>}
|
title={`Reindex Playlist ${playlist.playlist_name}`}
|
||||||
{!reindex && (
|
onClick={async () => {
|
||||||
<div id="reindex-button" className="button-box">
|
setReindex(true);
|
||||||
{!isCustomPlaylist && (
|
|
||||||
<Button
|
await queueReindex(playlist.playlist_id, 'playlist');
|
||||||
label="Reindex"
|
}}
|
||||||
title={`Reindex Playlist ${playlist.playlist_name}`}
|
/>
|
||||||
onClick={async () => {
|
)}{' '}
|
||||||
setReindex(true);
|
<Button
|
||||||
|
label="Reindex Videos"
|
||||||
await queueReindex(playlist.playlist_id, 'playlist');
|
title={`Reindex Videos of ${playlist.playlist_name}`}
|
||||||
}}
|
onClick={async () => {
|
||||||
/>
|
setReindex(true);
|
||||||
)}{' '}
|
|
||||||
<Button
|
await queueReindex(playlist.playlist_id, 'playlist', true);
|
||||||
label="Reindex Videos"
|
}}
|
||||||
title={`Reindex Videos of ${playlist.playlist_name}`}
|
/>
|
||||||
onClick={async () => {
|
</div>
|
||||||
setReindex(true);
|
)}
|
||||||
|
</div>
|
||||||
await queueReindex(playlist.playlist_id, 'playlist', true);
|
</div>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
{playlist.playlist_description && (
|
||||||
)}
|
<div className="description-box">
|
||||||
</div>
|
<p
|
||||||
</div>
|
id={descriptionExpanded ? 'text-expand-expanded' : 'text-expand'}
|
||||||
</div>
|
className="description-text"
|
||||||
|
>
|
||||||
{playlist.playlist_description && (
|
<Linkify>{playlist.playlist_description}</Linkify>
|
||||||
<div className="description-box">
|
</p>
|
||||||
<p
|
|
||||||
id={descriptionExpanded ? 'text-expand-expanded' : 'text-expand'}
|
<Button
|
||||||
className="description-text"
|
label="Show more"
|
||||||
>
|
id="text-expand-button"
|
||||||
<Linkify>{playlist.playlist_description}</Linkify>
|
onClick={() => setDescriptionExpanded(!descriptionExpanded)}
|
||||||
</p>
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
)}
|
||||||
label="Show more"
|
</div>
|
||||||
id="text-expand-button"
|
|
||||||
onClick={() => setDescriptionExpanded(!descriptionExpanded)}
|
<div className={`boxed-content ${gridView}`}>
|
||||||
/>
|
<Filterbar
|
||||||
</div>
|
hideToggleText="Hide watched videos:"
|
||||||
)}
|
hideWatched={hideWatched}
|
||||||
</div>
|
isGridView={isGridView}
|
||||||
|
view={view}
|
||||||
<div className={`boxed-content ${gridView}`}>
|
gridItems={gridItems}
|
||||||
<Filterbar
|
userMeConfig={userMeConfig}
|
||||||
hideToggleText="Hide watched videos:"
|
setHideWatched={setHideWatched}
|
||||||
hideWatched={hideWatched}
|
setView={setView}
|
||||||
isGridView={isGridView}
|
setGridItems={setGridItems}
|
||||||
view={view}
|
viewStyleName={ViewStyleNames.playlist}
|
||||||
gridItems={gridItems}
|
setRefresh={setRefresh}
|
||||||
userMeConfig={userMeConfig}
|
/>
|
||||||
setHideWatched={setHideWatched}
|
</div>
|
||||||
setView={setView}
|
|
||||||
setGridItems={setGridItems}
|
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
||||||
viewStyleName={ViewStyleNames.playlist}
|
|
||||||
setRefresh={setRefresh}
|
<div className={`boxed-content ${gridView}`}>
|
||||||
/>
|
<div className={`video-list ${view} ${gridViewGrid}`}>
|
||||||
</div>
|
{videoInPlaylistCount === 0 && (
|
||||||
|
<>
|
||||||
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
<h2>No videos found...</h2>
|
||||||
|
{isCustomPlaylist && (
|
||||||
<div className={`boxed-content ${gridView}`}>
|
<p>
|
||||||
<div className={`video-list ${view} ${gridViewGrid}`}>
|
Try going to the <a href="{% url 'home' %}">home page</a> to add videos to this
|
||||||
{videoInPlaylistCount === 0 && (
|
playlist.
|
||||||
<>
|
</p>
|
||||||
<h2>No videos found...</h2>
|
)}
|
||||||
{isCustomPlaylist && (
|
|
||||||
<p>
|
{!isCustomPlaylist && (
|
||||||
Try going to the <a href="{% url 'home' %}">home page</a> to add videos to this
|
<p>
|
||||||
playlist.
|
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the
|
||||||
</p>
|
scan and download tasks.
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
{!isCustomPlaylist && (
|
</>
|
||||||
<p>
|
)}
|
||||||
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the
|
{videoInPlaylistCount !== 0 && (
|
||||||
scan and download tasks.
|
<VideoList
|
||||||
</p>
|
videoList={videos}
|
||||||
)}
|
viewLayout={view}
|
||||||
</>
|
playlistId={playlistId}
|
||||||
)}
|
showReorderButton={isCustomPlaylist}
|
||||||
{videoInPlaylistCount !== 0 && (
|
refreshVideoList={setRefresh}
|
||||||
<VideoList
|
/>
|
||||||
videoList={videos}
|
)}
|
||||||
viewLayout={view}
|
</div>
|
||||||
playlistId={playlistId}
|
</div>
|
||||||
showReorderButton={isCustomPlaylist}
|
|
||||||
refreshVideoList={setRefresh}
|
<div className="boxed-content">
|
||||||
/>
|
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
);
|
||||||
|
};
|
||||||
<div className="boxed-content">
|
|
||||||
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
|
export default Playlist;
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Playlist;
|
|
||||||
|
@ -1,196 +1,194 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
import iconAdd from '/img/icon-add.svg';
|
import iconAdd from '/img/icon-add.svg';
|
||||||
import iconGridView from '/img/icon-gridview.svg';
|
import iconGridView from '/img/icon-gridview.svg';
|
||||||
import iconListView from '/img/icon-listview.svg';
|
import iconListView from '/img/icon-listview.svg';
|
||||||
|
|
||||||
import { OutletContextType } from './Base';
|
import { OutletContextType } from './Base';
|
||||||
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
||||||
import { ConfigType } from './Home';
|
import { ConfigType } from './Home';
|
||||||
import Pagination, { PaginationType } from '../components/Pagination';
|
import Pagination, { PaginationType } from '../components/Pagination';
|
||||||
import PlaylistList from '../components/PlaylistList';
|
import PlaylistList from '../components/PlaylistList';
|
||||||
import { PlaylistType } from './Playlist';
|
import { PlaylistType } from './Playlist';
|
||||||
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
|
||||||
import createCustomPlaylist from '../api/actions/createCustomPlaylist';
|
import createCustomPlaylist from '../api/actions/createCustomPlaylist';
|
||||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import loadIsAdmin from '../functions/getIsAdmin';
|
import loadIsAdmin from '../functions/getIsAdmin';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
export type PlaylistEntryType = {
|
export type PlaylistEntryType = {
|
||||||
youtube_id: string;
|
youtube_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
uploader: string;
|
uploader: string;
|
||||||
idx: number;
|
idx: number;
|
||||||
downloaded: boolean;
|
downloaded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlaylistsResponseType = {
|
export type PlaylistsResponseType = {
|
||||||
data?: PlaylistType[];
|
data?: PlaylistType[];
|
||||||
config?: ConfigType;
|
config?: ConfigType;
|
||||||
paginate?: PaginationType;
|
paginate?: PaginationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Playlists = () => {
|
const Playlists = () => {
|
||||||
const { userConfig, setPartialConfig } = useUserConfigStore();
|
const { userConfig, setPartialConfig } = useUserConfigStore();
|
||||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||||
const isAdmin = loadIsAdmin();
|
const isAdmin = loadIsAdmin();
|
||||||
|
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refresh, setRefresh] = useState(false);
|
||||||
const [playlistsToAddText, setPlaylistsToAddText] = useState('');
|
const [playlistsToAddText, setPlaylistsToAddText] = useState('');
|
||||||
const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
|
const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
|
||||||
|
|
||||||
const [playlistResponse, setPlaylistReponse] = useState<PlaylistsResponseType>();
|
const [playlistResponse, setPlaylistReponse] = useState<PlaylistsResponseType>();
|
||||||
|
|
||||||
const playlistList = playlistResponse?.data;
|
const playlistList = playlistResponse?.data;
|
||||||
const pagination = playlistResponse?.paginate;
|
const pagination = playlistResponse?.paginate;
|
||||||
|
|
||||||
const hasPlaylists = playlistResponse?.data?.length !== 0;
|
const hasPlaylists = playlistResponse?.data?.length !== 0;
|
||||||
|
|
||||||
const view = userConfig.config.view_style_playlist;
|
const view = userConfig.config.view_style_playlist;
|
||||||
const showSubedOnly = userConfig.config.show_subed_only;
|
const showSubedOnly = userConfig.config.show_subed_only;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const playlist = await loadPlaylistList({
|
const playlist = await loadPlaylistList({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
subscribed: showSubedOnly,
|
subscribed: showSubedOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
setPlaylistReponse(playlist);
|
setPlaylistReponse(playlist);
|
||||||
setRefresh(false);
|
setRefresh(false);
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
|
}, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | Playlists</title>
|
<title>TA | Playlists</title>
|
||||||
<ScrollToTopOnNavigate />
|
<ScrollToTopOnNavigate />
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<div className="title-split">
|
<div className="title-split">
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Playlists</h1>
|
<h1>Playlists</h1>
|
||||||
</div>
|
</div>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="title-split-form">
|
<div className="title-split-form">
|
||||||
<img
|
<img
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAddForm(!showAddForm);
|
setShowAddForm(!showAddForm);
|
||||||
}}
|
}}
|
||||||
src={iconAdd}
|
src={iconAdd}
|
||||||
alt="add-icon"
|
alt="add-icon"
|
||||||
title="Subscribe to Playlists"
|
title="Subscribe to Playlists"
|
||||||
/>
|
/>
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="show-form">
|
<div className="show-form">
|
||||||
<div>
|
<div>
|
||||||
<label>Subscribe to playlists:</label>
|
<label>Subscribe to playlists:</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={playlistsToAddText}
|
value={playlistsToAddText}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setPlaylistsToAddText(event.target.value);
|
setPlaylistsToAddText(event.target.value);
|
||||||
}}
|
}}
|
||||||
rows={3}
|
rows={3}
|
||||||
cols={40}
|
cols={40}
|
||||||
placeholder="Input playlist IDs or URLs"
|
placeholder="Input playlist IDs or URLs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label="Subscribe"
|
label="Subscribe"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updatePlaylistSubscription(playlistsToAddText, true);
|
await updatePlaylistSubscription(playlistsToAddText, true);
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<label>Or create custom playlist:</label>
|
<label>Or create custom playlist:</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={1}
|
rows={1}
|
||||||
cols={40}
|
cols={40}
|
||||||
placeholder="Input playlist name"
|
placeholder="Input playlist name"
|
||||||
value={customPlaylistsToAddText}
|
value={customPlaylistsToAddText}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setCustomPlaylistsToAddText(event.target.value);
|
setCustomPlaylistsToAddText(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label="Create"
|
label="Create"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createCustomPlaylist(customPlaylistsToAddText);
|
await createCustomPlaylist(customPlaylistsToAddText);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="notifications"></div>
|
<div id="notifications"></div>
|
||||||
|
|
||||||
<div className="view-controls">
|
<div className="view-controls">
|
||||||
<div className="toggle">
|
<div className="toggle">
|
||||||
<span>Show subscribed only:</span>
|
<span>Show subscribed only:</span>
|
||||||
<div className="toggleBox">
|
<div className="toggleBox">
|
||||||
<input
|
<input
|
||||||
checked={showSubedOnly}
|
checked={showSubedOnly}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setPartialConfig({show_subed_only: !showSubedOnly});
|
setPartialConfig({ show_subed_only: !showSubedOnly });
|
||||||
}}
|
}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
{!showSubedOnly && (
|
{!showSubedOnly && (
|
||||||
<label htmlFor="" className="ofbtn">
|
<label htmlFor="" className="ofbtn">
|
||||||
Off
|
Off
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{showSubedOnly && (
|
{showSubedOnly && (
|
||||||
<label htmlFor="" className="onbtn">
|
<label htmlFor="" className="onbtn">
|
||||||
On
|
On
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="view-icons">
|
<div className="view-icons">
|
||||||
<img
|
<img
|
||||||
src={iconGridView}
|
src={iconGridView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({view_style_playlist: 'grid'});
|
setPartialConfig({ view_style_playlist: 'grid' });
|
||||||
}}
|
}}
|
||||||
alt="grid view"
|
alt="grid view"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={iconListView}
|
src={iconListView}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartialConfig({view_style_playlist: 'list'});
|
setPartialConfig({ view_style_playlist: 'list' });
|
||||||
}}
|
}}
|
||||||
alt="list view"
|
alt="list view"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`playlist-list ${view}`}>
|
<div className={`playlist-list ${view}`}>
|
||||||
{!hasPlaylists && <h2>No playlists found...</h2>}
|
{!hasPlaylists && <h2>No playlists found...</h2>}
|
||||||
|
|
||||||
{hasPlaylists && (
|
{hasPlaylists && <PlaylistList playlistList={playlistList} setRefresh={setRefresh} />}
|
||||||
<PlaylistList playlistList={playlistList} setRefresh={setRefresh} />
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div className="boxed-content">
|
||||||
|
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
|
||||||
<div className="boxed-content">
|
</div>
|
||||||
{pagination && <Pagination pagination={pagination} setPage={setCurrentPage} />}
|
</>
|
||||||
</div>
|
);
|
||||||
</>
|
};
|
||||||
);
|
|
||||||
};
|
export default Playlists;
|
||||||
|
|
||||||
export default Playlists;
|
|
||||||
|
@ -1,163 +1,164 @@
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { VideoType } from './Home';
|
import { VideoType } from './Home';
|
||||||
import loadSearch from '../api/loader/loadSearch';
|
import loadSearch from '../api/loader/loadSearch';
|
||||||
import { PlaylistType } from './Playlist';
|
import { PlaylistType } from './Playlist';
|
||||||
import { ChannelType } from './Channels';
|
import { ChannelType } from './Channels';
|
||||||
import VideoList from '../components/VideoList';
|
import VideoList from '../components/VideoList';
|
||||||
import ChannelList from '../components/ChannelList';
|
import ChannelList from '../components/ChannelList';
|
||||||
import PlaylistList from '../components/PlaylistList';
|
import PlaylistList from '../components/PlaylistList';
|
||||||
import SubtitleList from '../components/SubtitleList';
|
import SubtitleList from '../components/SubtitleList';
|
||||||
import { ViewStyles } from '../configuration/constants/ViewStyle';
|
import { ViewStyles } from '../configuration/constants/ViewStyle';
|
||||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||||
import SearchExampleQueries from '../components/SearchExampleQueries';
|
import SearchExampleQueries from '../components/SearchExampleQueries';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
|
||||||
const EmptySearchResponse: SearchResultsType = {
|
const EmptySearchResponse: SearchResultsType = {
|
||||||
results: {
|
results: {
|
||||||
video_results: [],
|
video_results: [],
|
||||||
channel_results: [],
|
channel_results: [],
|
||||||
playlist_results: [],
|
playlist_results: [],
|
||||||
fulltext_results: [],
|
fulltext_results: [],
|
||||||
},
|
},
|
||||||
queryType: 'simple',
|
queryType: 'simple',
|
||||||
};
|
};
|
||||||
|
|
||||||
type SearchResultType = {
|
type SearchResultType = {
|
||||||
video_results: VideoType[];
|
video_results: VideoType[];
|
||||||
channel_results: ChannelType[];
|
channel_results: ChannelType[];
|
||||||
playlist_results: PlaylistType[];
|
playlist_results: PlaylistType[];
|
||||||
fulltext_results: [];
|
fulltext_results: [];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SearchResultsType = {
|
type SearchResultsType = {
|
||||||
results: SearchResultType;
|
results: SearchResultType;
|
||||||
queryType: string;
|
queryType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const { userConfig } = useUserConfigStore();
|
const { userConfig } = useUserConfigStore();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const videoId = searchParams.get('videoId');
|
const videoId = searchParams.get('videoId');
|
||||||
const userMeConfig = userConfig.config;
|
const userMeConfig = userConfig.config;
|
||||||
|
|
||||||
const viewVideos = userMeConfig.view_style_home;
|
const viewVideos = userMeConfig.view_style_home;
|
||||||
const viewChannels = userMeConfig.view_style_channel;
|
const viewChannels = userMeConfig.view_style_channel;
|
||||||
const viewPlaylists = userMeConfig.view_style_playlist;
|
const viewPlaylists = userMeConfig.view_style_playlist;
|
||||||
const gridItems = userMeConfig.grid_items || 3;
|
const gridItems = userMeConfig.grid_items || 3;
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState<SearchResultsType>();
|
const [searchResults, setSearchResults] = useState<SearchResultsType>();
|
||||||
|
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
|
||||||
const videoList = searchResults?.results.video_results;
|
const videoList = searchResults?.results.video_results;
|
||||||
const channelList = searchResults?.results.channel_results;
|
const channelList = searchResults?.results.channel_results;
|
||||||
const playlistList = searchResults?.results.playlist_results;
|
const playlistList = searchResults?.results.playlist_results;
|
||||||
const fulltextList = searchResults?.results.fulltext_results;
|
const fulltextList = searchResults?.results.fulltext_results;
|
||||||
const queryType = searchResults?.queryType;
|
const queryType = searchResults?.queryType;
|
||||||
const showEmbeddedVideo = videoId !== null;
|
const showEmbeddedVideo = videoId !== null;
|
||||||
|
|
||||||
const hasSearchQuery = searchQuery.length > 0;
|
const hasSearchQuery = searchQuery.length > 0;
|
||||||
const hasVideos = Number(videoList?.length) > 0;
|
const hasVideos = Number(videoList?.length) > 0;
|
||||||
const hasChannels = Number(channelList?.length) > 0;
|
const hasChannels = Number(channelList?.length) > 0;
|
||||||
const hasPlaylist = Number(playlistList?.length) > 0;
|
const hasPlaylist = Number(playlistList?.length) > 0;
|
||||||
const hasFulltext = Number(fulltextList?.length) > 0;
|
const hasFulltext = Number(fulltextList?.length) > 0;
|
||||||
|
|
||||||
const isSimpleQuery = queryType === 'simple';
|
const isSimpleQuery = queryType === 'simple';
|
||||||
const isVideoQuery = queryType === 'video' || isSimpleQuery;
|
const isVideoQuery = queryType === 'video' || isSimpleQuery;
|
||||||
const isChannelQuery = queryType === 'channel' || isSimpleQuery;
|
const isChannelQuery = queryType === 'channel' || isSimpleQuery;
|
||||||
const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
|
const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
|
||||||
const isFullTextQuery = queryType === 'full' || isSimpleQuery;
|
const isFullTextQuery = queryType === 'full' || isSimpleQuery;
|
||||||
|
|
||||||
const isGridView = viewVideos === ViewStyles.grid;
|
const isGridView = viewVideos === ViewStyles.grid;
|
||||||
const gridView = isGridView ? `boxed-${gridItems}` : '';
|
const gridView = isGridView ? `boxed-${gridItems}` : '';
|
||||||
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
|
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!hasSearchQuery) {
|
if (!hasSearchQuery) {
|
||||||
setSearchResults(EmptySearchResponse);
|
setSearchResults(EmptySearchResponse);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResults = await loadSearch(searchQuery);
|
const searchResults = await loadSearch(searchQuery);
|
||||||
|
|
||||||
setSearchResults(searchResults);
|
setSearchResults(searchResults);
|
||||||
setRefresh(false);
|
setRefresh(false);
|
||||||
})();
|
})();
|
||||||
}, [searchQuery, refresh, hasSearchQuery]);
|
}, [searchQuery, refresh, hasSearchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TubeArchivist</title>
|
<title>TubeArchivist</title>
|
||||||
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
{showEmbeddedVideo && <EmbeddableVideoPlayer videoId={videoId} />}
|
||||||
<div className={`boxed-content ${gridView}`}>
|
<div className={`boxed-content ${gridView}`}>
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Search your Archive</h1>
|
<h1>Search your Archive</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="multi-search-box">
|
<div className="multi-search-box">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setSearchQuery(event.target.value);
|
setSearchQuery(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="multi-search-results">
|
<div id="multi-search-results">
|
||||||
{hasSearchQuery && isVideoQuery && (
|
{hasSearchQuery && isVideoQuery && (
|
||||||
<div className="multi-search-result">
|
<div className="multi-search-result">
|
||||||
<h2>Video Results</h2>
|
<h2>Video Results</h2>
|
||||||
<div id="video-results" className={`video-list ${viewVideos} ${gridViewGrid}`}>
|
<div id="video-results" className={`video-list ${viewVideos} ${gridViewGrid}`}>
|
||||||
<VideoList videoList={videoList} viewLayout={viewVideos} refreshVideoList={setRefresh} />
|
<VideoList
|
||||||
</div>
|
videoList={videoList}
|
||||||
</div>
|
viewLayout={viewVideos}
|
||||||
)}
|
refreshVideoList={setRefresh}
|
||||||
|
/>
|
||||||
{hasSearchQuery && isChannelQuery && (
|
</div>
|
||||||
<div className="multi-search-result">
|
</div>
|
||||||
<h2>Channel Results</h2>
|
)}
|
||||||
<div id="channel-results" className={`channel-list ${viewChannels} ${gridViewGrid}`}>
|
|
||||||
<ChannelList
|
{hasSearchQuery && isChannelQuery && (
|
||||||
channelList={channelList}
|
<div className="multi-search-result">
|
||||||
refreshChannelList={setRefresh}
|
<h2>Channel Results</h2>
|
||||||
/>
|
<div id="channel-results" className={`channel-list ${viewChannels} ${gridViewGrid}`}>
|
||||||
</div>
|
<ChannelList channelList={channelList} refreshChannelList={setRefresh} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{hasSearchQuery && isPlaylistQuery && (
|
|
||||||
<div className="multi-search-result">
|
{hasSearchQuery && isPlaylistQuery && (
|
||||||
<h2>Playlist Results</h2>
|
<div className="multi-search-result">
|
||||||
<div id="playlist-results" className={`playlist-list ${viewPlaylists} ${gridViewGrid}`}>
|
<h2>Playlist Results</h2>
|
||||||
<PlaylistList
|
<div
|
||||||
playlistList={playlistList}
|
id="playlist-results"
|
||||||
setRefresh={setRefresh}
|
className={`playlist-list ${viewPlaylists} ${gridViewGrid}`}
|
||||||
/>
|
>
|
||||||
</div>
|
<PlaylistList playlistList={playlistList} setRefresh={setRefresh} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{hasSearchQuery && isFullTextQuery && (
|
|
||||||
<div className="multi-search-result">
|
{hasSearchQuery && isFullTextQuery && (
|
||||||
<h2>Fulltext Results</h2>
|
<div className="multi-search-result">
|
||||||
<div id="fulltext-results" className="video-list list">
|
<h2>Fulltext Results</h2>
|
||||||
<SubtitleList subtitleList={fulltextList} />
|
<div id="fulltext-results" className="video-list list">
|
||||||
</div>
|
<SubtitleList subtitleList={fulltextList} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
{!hasVideos && !hasChannels && !hasPlaylist && !hasFulltext && <SearchExampleQueries />}
|
|
||||||
</div>
|
{!hasVideos && !hasChannels && !hasPlaylist && !hasFulltext && <SearchExampleQueries />}
|
||||||
</>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
export default Search;
|
|
||||||
|
export default Search;
|
||||||
|
@ -1,242 +1,242 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import loadBackupList from '../api/loader/loadBackupList';
|
import loadBackupList from '../api/loader/loadBackupList';
|
||||||
import SettingsNavigation from '../components/SettingsNavigation';
|
import SettingsNavigation from '../components/SettingsNavigation';
|
||||||
import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
|
import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
|
||||||
import updateTaskByName from '../api/actions/updateTaskByName';
|
import updateTaskByName from '../api/actions/updateTaskByName';
|
||||||
import queueBackup from '../api/actions/queueBackup';
|
import queueBackup from '../api/actions/queueBackup';
|
||||||
import restoreBackup from '../api/actions/restoreBackup';
|
import restoreBackup from '../api/actions/restoreBackup';
|
||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
|
|
||||||
type Backup = {
|
type Backup = {
|
||||||
filename: string;
|
filename: string;
|
||||||
file_path: string;
|
file_path: string;
|
||||||
file_size: number;
|
file_size: number;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BackupListType = Backup[];
|
type BackupListType = Backup[];
|
||||||
|
|
||||||
const SettingsActions = () => {
|
const SettingsActions = () => {
|
||||||
const [deleteIgnored, setDeleteIgnored] = useState(false);
|
const [deleteIgnored, setDeleteIgnored] = useState(false);
|
||||||
const [deletePending, setDeletePending] = useState(false);
|
const [deletePending, setDeletePending] = useState(false);
|
||||||
const [processingImports, setProcessingImports] = useState(false);
|
const [processingImports, setProcessingImports] = useState(false);
|
||||||
const [reEmbed, setReEmbed] = useState(false);
|
const [reEmbed, setReEmbed] = useState(false);
|
||||||
const [backupStarted, setBackupStarted] = useState(false);
|
const [backupStarted, setBackupStarted] = useState(false);
|
||||||
const [isRestoringBackup, setIsRestoringBackup] = useState(false);
|
const [isRestoringBackup, setIsRestoringBackup] = useState(false);
|
||||||
const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
|
const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
|
||||||
|
|
||||||
const [backupListResponse, setBackupListResponse] = useState<BackupListType>();
|
const [backupListResponse, setBackupListResponse] = useState<BackupListType>();
|
||||||
|
|
||||||
const backups = backupListResponse;
|
const backups = backupListResponse;
|
||||||
const hasBackups = !!backups && backups?.length > 0;
|
const hasBackups = !!backups && backups?.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const backupListResponse = await loadBackupList();
|
const backupListResponse = await loadBackupList();
|
||||||
|
|
||||||
setBackupListResponse(backupListResponse);
|
setBackupListResponse(backupListResponse);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | Actions</title>
|
<title>TA | Actions</title>
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<SettingsNavigation />
|
<SettingsNavigation />
|
||||||
<Notifications
|
<Notifications
|
||||||
pageName={'all'}
|
pageName={'all'}
|
||||||
update={
|
update={
|
||||||
deleteIgnored ||
|
deleteIgnored ||
|
||||||
deletePending ||
|
deletePending ||
|
||||||
processingImports ||
|
processingImports ||
|
||||||
reEmbed ||
|
reEmbed ||
|
||||||
backupStarted ||
|
backupStarted ||
|
||||||
isRestoringBackup ||
|
isRestoringBackup ||
|
||||||
reScanningFileSystem
|
reScanningFileSystem
|
||||||
}
|
}
|
||||||
setShouldRefresh={() => {
|
setShouldRefresh={() => {
|
||||||
setDeleteIgnored(false);
|
setDeleteIgnored(false);
|
||||||
setDeletePending(false);
|
setDeletePending(false);
|
||||||
setProcessingImports(false);
|
setProcessingImports(false);
|
||||||
setReEmbed(false);
|
setReEmbed(false);
|
||||||
setBackupStarted(false);
|
setBackupStarted(false);
|
||||||
setIsRestoringBackup(false);
|
setIsRestoringBackup(false);
|
||||||
setReScanningFileSystem(false);
|
setReScanningFileSystem(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Actions</h1>
|
<h1>Actions</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Delete download queue</h2>
|
<h2>Delete download queue</h2>
|
||||||
<p>Delete your pending or previously ignored videos from your download queue.</p>
|
<p>Delete your pending or previously ignored videos from your download queue.</p>
|
||||||
{deleteIgnored && <p>Deleting download queue: ignored</p>}
|
{deleteIgnored && <p>Deleting download queue: ignored</p>}
|
||||||
{!deleteIgnored && (
|
{!deleteIgnored && (
|
||||||
<Button
|
<Button
|
||||||
label="Delete all ignored"
|
label="Delete all ignored"
|
||||||
title="Delete all previously ignored videos from the queue"
|
title="Delete all previously ignored videos from the queue"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteDownloadQueueByFilter('ignore');
|
await deleteDownloadQueueByFilter('ignore');
|
||||||
setDeleteIgnored(true);
|
setDeleteIgnored(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{deletePending && <p>Deleting download queue: pending</p>}
|
{deletePending && <p>Deleting download queue: pending</p>}
|
||||||
{!deletePending && (
|
{!deletePending && (
|
||||||
<Button
|
<Button
|
||||||
label="Delete all queued"
|
label="Delete all queued"
|
||||||
title="Delete all pending videos from the queue"
|
title="Delete all pending videos from the queue"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteDownloadQueueByFilter('pending');
|
await deleteDownloadQueueByFilter('pending');
|
||||||
setDeletePending(true);
|
setDeletePending(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Manual media files import.</h2>
|
<h2>Manual media files import.</h2>
|
||||||
<p>
|
<p>
|
||||||
Add files to the <span className="settings-current">cache/import</span> folder. Make
|
Add files to the <span className="settings-current">cache/import</span> folder. Make
|
||||||
sure to follow the instructions in the Github{' '}
|
sure to follow the instructions in the Github{' '}
|
||||||
<a
|
<a
|
||||||
href="https://docs.tubearchivist.com/settings/actions/#manual-media-files-import"
|
href="https://docs.tubearchivist.com/settings/actions/#manual-media-files-import"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Wiki
|
Wiki
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<div id="manual-import">
|
<div id="manual-import">
|
||||||
{processingImports && <p>Processing import</p>}
|
{processingImports && <p>Processing import</p>}
|
||||||
{!processingImports && (
|
{!processingImports && (
|
||||||
<Button
|
<Button
|
||||||
label="Start import"
|
label="Start import"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateTaskByName('manual_import');
|
await updateTaskByName('manual_import');
|
||||||
setProcessingImports(true);
|
setProcessingImports(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Embed thumbnails into media file.</h2>
|
<h2>Embed thumbnails into media file.</h2>
|
||||||
<p>Set extracted youtube thumbnail as cover art of the media file.</p>
|
<p>Set extracted youtube thumbnail as cover art of the media file.</p>
|
||||||
<div id="re-embed">
|
<div id="re-embed">
|
||||||
{reEmbed && <p>Processing thumbnails</p>}
|
{reEmbed && <p>Processing thumbnails</p>}
|
||||||
{!reEmbed && (
|
{!reEmbed && (
|
||||||
<Button
|
<Button
|
||||||
label="Start process"
|
label="Start process"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateTaskByName('resync_thumbs');
|
await updateTaskByName('resync_thumbs');
|
||||||
setReEmbed(true);
|
setReEmbed(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>ZIP file index backup</h2>
|
<h2>ZIP file index backup</h2>
|
||||||
<p>
|
<p>
|
||||||
Export your database to a zip file stored at{' '}
|
Export your database to a zip file stored at{' '}
|
||||||
<span className="settings-current">cache/backup</span>.
|
<span className="settings-current">cache/backup</span>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<i>
|
<i>
|
||||||
Zip file backups are very slow for large archives and consistency is not guaranteed,
|
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
|
use snapshots instead. Make sure no other tasks are running when creating a Zip file
|
||||||
backup.
|
backup.
|
||||||
</i>
|
</i>
|
||||||
</p>
|
</p>
|
||||||
<div id="db-backup">
|
<div id="db-backup">
|
||||||
{backupStarted && <p>Backing up archive</p>}
|
{backupStarted && <p>Backing up archive</p>}
|
||||||
{!backupStarted && (
|
{!backupStarted && (
|
||||||
<Button
|
<Button
|
||||||
label="Start backup"
|
label="Start backup"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await queueBackup();
|
await queueBackup();
|
||||||
setBackupStarted(true);
|
setBackupStarted(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Restore from backup</h2>
|
<h2>Restore from backup</h2>
|
||||||
<p>
|
<p>
|
||||||
<span className="danger-zone">Danger Zone</span>: This will replace your existing index
|
<span className="danger-zone">Danger Zone</span>: This will replace your existing index
|
||||||
with the backup.
|
with the backup.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Restore from available backup files from{' '}
|
Restore from available backup files from{' '}
|
||||||
<span className="settings-current">cache/backup</span>.
|
<span className="settings-current">cache/backup</span>.
|
||||||
</p>
|
</p>
|
||||||
{!hasBackups && <p>No backups found.</p>}
|
{!hasBackups && <p>No backups found.</p>}
|
||||||
{hasBackups && (
|
{hasBackups && (
|
||||||
<>
|
<>
|
||||||
<div className="backup-grid-row">
|
<div className="backup-grid-row">
|
||||||
<span></span>
|
<span></span>
|
||||||
<span>Timestamp</span>
|
<span>Timestamp</span>
|
||||||
<span>Source</span>
|
<span>Source</span>
|
||||||
<span>Filename</span>
|
<span>Filename</span>
|
||||||
</div>
|
</div>
|
||||||
{isRestoringBackup && <p>Restoring from backup</p>}
|
{isRestoringBackup && <p>Restoring from backup</p>}
|
||||||
{!isRestoringBackup &&
|
{!isRestoringBackup &&
|
||||||
backups.map(backup => {
|
backups.map(backup => {
|
||||||
return (
|
return (
|
||||||
<div key={backup.filename} id={backup.filename} className="backup-grid-row">
|
<div key={backup.filename} id={backup.filename} className="backup-grid-row">
|
||||||
<Button
|
<Button
|
||||||
label="Restore"
|
label="Restore"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await restoreBackup(backup.filename);
|
await restoreBackup(backup.filename);
|
||||||
setIsRestoringBackup(true);
|
setIsRestoringBackup(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span>{backup.timestamp}</span>
|
<span>{backup.timestamp}</span>
|
||||||
<span>{backup.reason}</span>
|
<span>{backup.reason}</span>
|
||||||
<span>{backup.filename}</span>
|
<span>{backup.filename}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Rescan filesystem</h2>
|
<h2>Rescan filesystem</h2>
|
||||||
<p>
|
<p>
|
||||||
<span className="danger-zone">Danger Zone</span>: This will delete the metadata of
|
<span className="danger-zone">Danger Zone</span>: This will delete the metadata of
|
||||||
deleted videos from the filesystem.
|
deleted videos from the filesystem.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Rescan your media folder looking for missing videos and clean up index. More infos on
|
Rescan your media folder looking for missing videos and clean up index. More infos on
|
||||||
the Github{' '}
|
the Github{' '}
|
||||||
<a
|
<a
|
||||||
href="https://docs.tubearchivist.com/settings/actions/#rescan-filesystem"
|
href="https://docs.tubearchivist.com/settings/actions/#rescan-filesystem"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Wiki
|
Wiki
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<div id="fs-rescan">
|
<div id="fs-rescan">
|
||||||
{reScanningFileSystem && <p>File system scan in progress</p>}
|
{reScanningFileSystem && <p>File system scan in progress</p>}
|
||||||
{!reScanningFileSystem && (
|
{!reScanningFileSystem && (
|
||||||
<Button
|
<Button
|
||||||
label="Rescan filesystem"
|
label="Rescan filesystem"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await updateTaskByName('rescan_filesystem');
|
await updateTaskByName('rescan_filesystem');
|
||||||
setReScanningFileSystem(true);
|
setReScanningFileSystem(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsActions;
|
export default SettingsActions;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,260 +1,260 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import SettingsNavigation from '../components/SettingsNavigation';
|
import SettingsNavigation from '../components/SettingsNavigation';
|
||||||
import loadStatsVideo from '../api/loader/loadStatsVideo';
|
import loadStatsVideo from '../api/loader/loadStatsVideo';
|
||||||
import loadStatsChannel from '../api/loader/loadStatsChannel';
|
import loadStatsChannel from '../api/loader/loadStatsChannel';
|
||||||
import loadStatsPlaylist from '../api/loader/loadStatsPlaylist';
|
import loadStatsPlaylist from '../api/loader/loadStatsPlaylist';
|
||||||
import loadStatsDownload from '../api/loader/loadStatsDownload';
|
import loadStatsDownload from '../api/loader/loadStatsDownload';
|
||||||
import loadStatsWatchProgress from '../api/loader/loadStatsWatchProgress';
|
import loadStatsWatchProgress from '../api/loader/loadStatsWatchProgress';
|
||||||
import loadStatsDownloadHistory from '../api/loader/loadStatsDownloadHistory';
|
import loadStatsDownloadHistory from '../api/loader/loadStatsDownloadHistory';
|
||||||
import loadStatsBiggestChannels from '../api/loader/loadStatsBiggestChannels';
|
import loadStatsBiggestChannels from '../api/loader/loadStatsBiggestChannels';
|
||||||
import OverviewStats from '../components/OverviewStats';
|
import OverviewStats from '../components/OverviewStats';
|
||||||
import VideoTypeStats from '../components/VideoTypeStats';
|
import VideoTypeStats from '../components/VideoTypeStats';
|
||||||
import ApplicationStats from '../components/ApplicationStats';
|
import ApplicationStats from '../components/ApplicationStats';
|
||||||
import WatchProgressStats from '../components/WatchProgressStats';
|
import WatchProgressStats from '../components/WatchProgressStats';
|
||||||
import DownloadHistoryStats from '../components/DownloadHistoryStats';
|
import DownloadHistoryStats from '../components/DownloadHistoryStats';
|
||||||
import BiggestChannelsStats from '../components/BiggestChannelsStats';
|
import BiggestChannelsStats from '../components/BiggestChannelsStats';
|
||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import PaginationDummy from '../components/PaginationDummy';
|
import PaginationDummy from '../components/PaginationDummy';
|
||||||
|
|
||||||
export type VideoStatsType = {
|
export type VideoStatsType = {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
type_videos: {
|
type_videos: {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
};
|
};
|
||||||
type_shorts: {
|
type_shorts: {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
};
|
};
|
||||||
active_true: {
|
active_true: {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
};
|
};
|
||||||
active_false: {
|
active_false: {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
};
|
};
|
||||||
type_streams: {
|
type_streams: {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelStatsType = {
|
export type ChannelStatsType = {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
active_true: number;
|
active_true: number;
|
||||||
subscribed_true: number;
|
subscribed_true: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlaylistStatsType = {
|
export type PlaylistStatsType = {
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
active_false: number;
|
active_false: number;
|
||||||
active_true: number;
|
active_true: number;
|
||||||
subscribed_true: number;
|
subscribed_true: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadStatsType = {
|
export type DownloadStatsType = {
|
||||||
pending: number;
|
pending: number;
|
||||||
pending_videos: number;
|
pending_videos: number;
|
||||||
pending_shorts: number;
|
pending_shorts: number;
|
||||||
pending_streams: number;
|
pending_streams: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WatchProgressStatsType = {
|
export type WatchProgressStatsType = {
|
||||||
total: {
|
total: {
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
items: number;
|
items: number;
|
||||||
};
|
};
|
||||||
unwatched: {
|
unwatched: {
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
items: number;
|
items: number;
|
||||||
};
|
};
|
||||||
watched: {
|
watched: {
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
items: number;
|
items: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type DownloadHistoryType = {
|
type DownloadHistoryType = {
|
||||||
date: string;
|
date: string;
|
||||||
count: number;
|
count: number;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadHistoryStatsType = DownloadHistoryType[];
|
export type DownloadHistoryStatsType = DownloadHistoryType[];
|
||||||
|
|
||||||
type BiggestChannelsType = {
|
type BiggestChannelsType = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
duration_str: string;
|
duration_str: string;
|
||||||
media_size: number;
|
media_size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BiggestChannelsStatsType = BiggestChannelsType[];
|
export type BiggestChannelsStatsType = BiggestChannelsType[];
|
||||||
|
|
||||||
type DashboardStatsReponses = {
|
type DashboardStatsReponses = {
|
||||||
videoStats?: VideoStatsType;
|
videoStats?: VideoStatsType;
|
||||||
channelStats?: ChannelStatsType;
|
channelStats?: ChannelStatsType;
|
||||||
playlistStats?: PlaylistStatsType;
|
playlistStats?: PlaylistStatsType;
|
||||||
downloadStats?: DownloadStatsType;
|
downloadStats?: DownloadStatsType;
|
||||||
watchProgressStats?: WatchProgressStatsType;
|
watchProgressStats?: WatchProgressStatsType;
|
||||||
downloadHistoryStats?: DownloadHistoryStatsType;
|
downloadHistoryStats?: DownloadHistoryStatsType;
|
||||||
biggestChannelsStatsByCount?: BiggestChannelsStatsType;
|
biggestChannelsStatsByCount?: BiggestChannelsStatsType;
|
||||||
biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
|
biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
|
||||||
biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
|
biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsDashboard = () => {
|
const SettingsDashboard = () => {
|
||||||
const [useSi, setUseSi] = useState(false);
|
const [useSi, setUseSi] = useState(false);
|
||||||
|
|
||||||
const [response, setResponse] = useState<DashboardStatsReponses>({
|
const [response, setResponse] = useState<DashboardStatsReponses>({
|
||||||
videoStats: undefined,
|
videoStats: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoStats = response?.videoStats;
|
const videoStats = response?.videoStats;
|
||||||
const channelStats = response?.channelStats;
|
const channelStats = response?.channelStats;
|
||||||
const playlistStats = response?.playlistStats;
|
const playlistStats = response?.playlistStats;
|
||||||
const downloadStats = response?.downloadStats;
|
const downloadStats = response?.downloadStats;
|
||||||
const watchProgressStats = response?.watchProgressStats;
|
const watchProgressStats = response?.watchProgressStats;
|
||||||
const downloadHistoryStats = response?.downloadHistoryStats;
|
const downloadHistoryStats = response?.downloadHistoryStats;
|
||||||
const biggestChannelsStatsByCount = response?.biggestChannelsStatsByCount;
|
const biggestChannelsStatsByCount = response?.biggestChannelsStatsByCount;
|
||||||
const biggestChannelsStatsByDuration = response?.biggestChannelsStatsByDuration;
|
const biggestChannelsStatsByDuration = response?.biggestChannelsStatsByDuration;
|
||||||
const biggestChannelsStatsByMediaSize = response?.biggestChannelsStatsByMediaSize;
|
const biggestChannelsStatsByMediaSize = response?.biggestChannelsStatsByMediaSize;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const all = await Promise.all([
|
const all = await Promise.all([
|
||||||
await loadStatsVideo(),
|
await loadStatsVideo(),
|
||||||
await loadStatsChannel(),
|
await loadStatsChannel(),
|
||||||
await loadStatsPlaylist(),
|
await loadStatsPlaylist(),
|
||||||
await loadStatsDownload(),
|
await loadStatsDownload(),
|
||||||
await loadStatsWatchProgress(),
|
await loadStatsWatchProgress(),
|
||||||
await loadStatsDownloadHistory(),
|
await loadStatsDownloadHistory(),
|
||||||
await loadStatsBiggestChannels('doc_count'),
|
await loadStatsBiggestChannels('doc_count'),
|
||||||
await loadStatsBiggestChannels('duration'),
|
await loadStatsBiggestChannels('duration'),
|
||||||
await loadStatsBiggestChannels('media_size'),
|
await loadStatsBiggestChannels('media_size'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
videoStats,
|
videoStats,
|
||||||
channelStats,
|
channelStats,
|
||||||
playlistStats,
|
playlistStats,
|
||||||
downloadStats,
|
downloadStats,
|
||||||
watchProgressStats,
|
watchProgressStats,
|
||||||
downloadHistoryStats,
|
downloadHistoryStats,
|
||||||
biggestChannelsStatsByCount,
|
biggestChannelsStatsByCount,
|
||||||
biggestChannelsStatsByDuration,
|
biggestChannelsStatsByDuration,
|
||||||
biggestChannelsStatsByMediaSize,
|
biggestChannelsStatsByMediaSize,
|
||||||
] = all;
|
] = all;
|
||||||
|
|
||||||
setResponse({
|
setResponse({
|
||||||
videoStats,
|
videoStats,
|
||||||
channelStats,
|
channelStats,
|
||||||
playlistStats,
|
playlistStats,
|
||||||
downloadStats,
|
downloadStats,
|
||||||
watchProgressStats,
|
watchProgressStats,
|
||||||
downloadHistoryStats,
|
downloadHistoryStats,
|
||||||
biggestChannelsStatsByCount,
|
biggestChannelsStatsByCount,
|
||||||
biggestChannelsStatsByDuration,
|
biggestChannelsStatsByDuration,
|
||||||
biggestChannelsStatsByMediaSize,
|
biggestChannelsStatsByMediaSize,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | Settings Dashboard</title>
|
<title>TA | Settings Dashboard</title>
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<SettingsNavigation />
|
<SettingsNavigation />
|
||||||
<Notifications pageName={'all'} />
|
<Notifications pageName={'all'} />
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Your Archive</h1>
|
<h1>Your Archive</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
File Sizes in:
|
File Sizes in:
|
||||||
<select
|
<select
|
||||||
value={useSi ? 'true' : 'false'}
|
value={useSi ? 'true' : 'false'}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
console.log(value);
|
console.log(value);
|
||||||
setUseSi(value === 'true');
|
setUseSi(value === 'true');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="true">SI units</option>
|
<option value="true">SI units</option>
|
||||||
<option value="false">Binary units</option>
|
<option value="false">Binary units</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h2>Overview</h2>
|
<h2>Overview</h2>
|
||||||
<div className="info-box info-box-3">
|
<div className="info-box info-box-3">
|
||||||
<OverviewStats videoStats={videoStats} useSI={useSi} />
|
<OverviewStats videoStats={videoStats} useSI={useSi} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h2>Video Type</h2>
|
<h2>Video Type</h2>
|
||||||
<div className="info-box info-box-3">
|
<div className="info-box info-box-3">
|
||||||
<VideoTypeStats videoStats={videoStats} useSI={useSi} />
|
<VideoTypeStats videoStats={videoStats} useSI={useSi} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h2>Application</h2>
|
<h2>Application</h2>
|
||||||
<div className="info-box info-box-3">
|
<div className="info-box info-box-3">
|
||||||
<ApplicationStats
|
<ApplicationStats
|
||||||
channelStats={channelStats}
|
channelStats={channelStats}
|
||||||
playlistStats={playlistStats}
|
playlistStats={playlistStats}
|
||||||
downloadStats={downloadStats}
|
downloadStats={downloadStats}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h2>Watch Progress</h2>
|
<h2>Watch Progress</h2>
|
||||||
<div className="info-box info-box-2">
|
<div className="info-box info-box-2">
|
||||||
<WatchProgressStats watchProgressStats={watchProgressStats} />
|
<WatchProgressStats watchProgressStats={watchProgressStats} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h2>Download History</h2>
|
<h2>Download History</h2>
|
||||||
<div className="info-box info-box-4">
|
<div className="info-box info-box-4">
|
||||||
<DownloadHistoryStats downloadHistoryStats={downloadHistoryStats} useSI={false} />
|
<DownloadHistoryStats downloadHistoryStats={downloadHistoryStats} useSI={false} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h2>Biggest Channels</h2>
|
<h2>Biggest Channels</h2>
|
||||||
<div className="info-box info-box-3">
|
<div className="info-box info-box-3">
|
||||||
<BiggestChannelsStats
|
<BiggestChannelsStats
|
||||||
biggestChannelsStatsByCount={biggestChannelsStatsByCount}
|
biggestChannelsStatsByCount={biggestChannelsStatsByCount}
|
||||||
biggestChannelsStatsByDuration={biggestChannelsStatsByDuration}
|
biggestChannelsStatsByDuration={biggestChannelsStatsByDuration}
|
||||||
biggestChannelsStatsByMediaSize={biggestChannelsStatsByMediaSize}
|
biggestChannelsStatsByMediaSize={biggestChannelsStatsByMediaSize}
|
||||||
useSI={useSi}
|
useSI={useSi}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PaginationDummy />
|
<PaginationDummy />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsDashboard;
|
export default SettingsDashboard;
|
||||||
|
@ -1,495 +1,495 @@
|
|||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import SettingsNavigation from '../components/SettingsNavigation';
|
import SettingsNavigation from '../components/SettingsNavigation';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import PaginationDummy from '../components/PaginationDummy';
|
import PaginationDummy from '../components/PaginationDummy';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import loadSchedule, { ScheduleResponseType } from '../api/loader/loadSchedule';
|
import loadSchedule, { ScheduleResponseType } from '../api/loader/loadSchedule';
|
||||||
import loadAppriseNotification, {
|
import loadAppriseNotification, {
|
||||||
AppriseNotificationType,
|
AppriseNotificationType,
|
||||||
} from '../api/loader/loadAppriseNotification';
|
} from '../api/loader/loadAppriseNotification';
|
||||||
import deleteTaskSchedule from '../api/actions/deleteTaskSchedule';
|
import deleteTaskSchedule from '../api/actions/deleteTaskSchedule';
|
||||||
import createTaskSchedule from '../api/actions/createTaskSchedule';
|
import createTaskSchedule from '../api/actions/createTaskSchedule';
|
||||||
import createAppriseNotificationUrl, {
|
import createAppriseNotificationUrl, {
|
||||||
AppriseTaskNameType,
|
AppriseTaskNameType,
|
||||||
} from '../api/actions/createAppriseNotificationUrl';
|
} from '../api/actions/createAppriseNotificationUrl';
|
||||||
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
||||||
|
|
||||||
const SettingsScheduling = () => {
|
const SettingsScheduling = () => {
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
|
||||||
const [scheduleResponse, setScheduleResponse] = useState<ScheduleResponseType>([]);
|
const [scheduleResponse, setScheduleResponse] = useState<ScheduleResponseType>([]);
|
||||||
const [appriseNotification, setAppriseNotification] = useState<AppriseNotificationType>();
|
const [appriseNotification, setAppriseNotification] = useState<AppriseNotificationType>();
|
||||||
|
|
||||||
const [updateSubscribed, setUpdateSubscribed] = useState<string | undefined>();
|
const [updateSubscribed, setUpdateSubscribed] = useState<string | undefined>();
|
||||||
const [downloadPending, setDownloadPending] = useState<string | undefined>();
|
const [downloadPending, setDownloadPending] = useState<string | undefined>();
|
||||||
const [checkReindex, setCheckReindex] = useState<string | undefined>();
|
const [checkReindex, setCheckReindex] = useState<string | undefined>();
|
||||||
const [checkReindexDays, setCheckReindexDays] = useState<number | undefined>();
|
const [checkReindexDays, setCheckReindexDays] = useState<number | undefined>();
|
||||||
const [thumbnailCheck, setThumbnailCheck] = useState<string | undefined>();
|
const [thumbnailCheck, setThumbnailCheck] = useState<string | undefined>();
|
||||||
const [zipBackup, setZipBackup] = useState<string | undefined>();
|
const [zipBackup, setZipBackup] = useState<string | undefined>();
|
||||||
const [zipBackupDays, setZipBackupDays] = useState<number | undefined>();
|
const [zipBackupDays, setZipBackupDays] = useState<number | undefined>();
|
||||||
const [notificationUrl, setNotificationUrl] = useState<string | undefined>();
|
const [notificationUrl, setNotificationUrl] = useState<string | undefined>();
|
||||||
const [notificationTask, setNotificationTask] = useState<AppriseTaskNameType | string>('');
|
const [notificationTask, setNotificationTask] = useState<AppriseTaskNameType | string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
const scheduleResponse = await loadSchedule();
|
const scheduleResponse = await loadSchedule();
|
||||||
const appriseNotificationResponse = await loadAppriseNotification();
|
const appriseNotificationResponse = await loadAppriseNotification();
|
||||||
|
|
||||||
setScheduleResponse(scheduleResponse);
|
setScheduleResponse(scheduleResponse);
|
||||||
setAppriseNotification(appriseNotificationResponse);
|
setAppriseNotification(appriseNotificationResponse);
|
||||||
|
|
||||||
setRefresh(false);
|
setRefresh(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
|
const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
|
||||||
|
|
||||||
console.log(groupedSchedules);
|
console.log(groupedSchedules);
|
||||||
|
|
||||||
const { update_subscribed, download_pending, run_backup, check_reindex, thumbnail_check } =
|
const { update_subscribed, download_pending, run_backup, check_reindex, thumbnail_check } =
|
||||||
groupedSchedules;
|
groupedSchedules;
|
||||||
|
|
||||||
const updateSubscribedSchedule = update_subscribed?.pop();
|
const updateSubscribedSchedule = update_subscribed?.pop();
|
||||||
const downloadPendingSchedule = download_pending?.pop();
|
const downloadPendingSchedule = download_pending?.pop();
|
||||||
const runBackup = run_backup?.pop();
|
const runBackup = run_backup?.pop();
|
||||||
const checkReindexSchedule = check_reindex?.pop();
|
const checkReindexSchedule = check_reindex?.pop();
|
||||||
const thumbnailCheckSchedule = thumbnail_check?.pop();
|
const thumbnailCheckSchedule = thumbnail_check?.pop();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | Scheduling Settings</title>
|
<title>TA | Scheduling Settings</title>
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<SettingsNavigation />
|
<SettingsNavigation />
|
||||||
<Notifications pageName={'all'} />
|
<Notifications pageName={'all'} />
|
||||||
|
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>Scheduler Setup</h1>
|
<h1>Scheduler Setup</h1>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<p>
|
<p>
|
||||||
Schedule settings expect a cron like format, where the first value is minute, second
|
Schedule settings expect a cron like format, where the first value is minute, second
|
||||||
is hour and third is day of the week.
|
is hour and third is day of the week.
|
||||||
</p>
|
</p>
|
||||||
<p>Examples:</p>
|
<p>Examples:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<span className="settings-current">0 15 *</span>: Run task every day at 15:00 in the
|
<span className="settings-current">0 15 *</span>: Run task every day at 15:00 in the
|
||||||
afternoon.
|
afternoon.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="settings-current">30 8 */2</span>: Run task every second day of the
|
<span className="settings-current">30 8 */2</span>: Run task every second day of the
|
||||||
week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
|
week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="settings-current">auto</span>: Sensible default.
|
<span className="settings-current">auto</span>: Sensible default.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Note:</p>
|
<p>Note:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that
|
Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that
|
||||||
reason, the scheduler doesn't support schedules that trigger more than once per
|
reason, the scheduler doesn't support schedules that trigger more than once per
|
||||||
hour.
|
hour.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Rescan Subscriptions</h2>
|
<h2>Rescan Subscriptions</h2>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
Become a sponsor and join{' '}
|
Become a sponsor and join{' '}
|
||||||
<a href="https://members.tubearchivist.com/" target="_blank">
|
<a href="https://members.tubearchivist.com/" target="_blank">
|
||||||
members.tubearchivist.com
|
members.tubearchivist.com
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
to get access to <span className="settings-current">real time</span> notifications for
|
to get access to <span className="settings-current">real time</span> notifications for
|
||||||
new videos uploaded by your favorite channels.
|
new videos uploaded by your favorite channels.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Current rescan schedule:{' '}
|
Current rescan schedule:{' '}
|
||||||
<span className="settings-current">
|
<span className="settings-current">
|
||||||
{!updateSubscribedSchedule && 'False'}
|
{!updateSubscribedSchedule && 'False'}
|
||||||
{updateSubscribedSchedule && (
|
{updateSubscribedSchedule && (
|
||||||
<>
|
<>
|
||||||
{updateSubscribedSchedule?.schedule}{' '}
|
{updateSubscribedSchedule?.schedule}{' '}
|
||||||
<Button
|
<Button
|
||||||
label="Delete"
|
label="Delete"
|
||||||
data-schedule="update_subscribed"
|
data-schedule="update_subscribed"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteTaskSchedule('update_subscribed');
|
await deleteTaskSchedule('update_subscribed');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Periodically rescan your subscriptions:</p>
|
<p>Periodically rescan your subscriptions:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={updateSubscribed || updateSubscribedSchedule?.schedule || ''}
|
value={updateSubscribed || updateSubscribedSchedule?.schedule || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setUpdateSubscribed(e.currentTarget.value);
|
setUpdateSubscribed(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('update_subscribed', {
|
await createTaskSchedule('update_subscribed', {
|
||||||
schedule: updateSubscribed,
|
schedule: updateSubscribed,
|
||||||
});
|
});
|
||||||
|
|
||||||
setUpdateSubscribed('');
|
setUpdateSubscribed('');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Start Download</h2>
|
<h2>Start Download</h2>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
Current Download schedule:{' '}
|
Current Download schedule:{' '}
|
||||||
<span className="settings-current">
|
<span className="settings-current">
|
||||||
{!download_pending && 'False'}
|
{!download_pending && 'False'}
|
||||||
{downloadPendingSchedule && (
|
{downloadPendingSchedule && (
|
||||||
<>
|
<>
|
||||||
{downloadPendingSchedule?.schedule}{' '}
|
{downloadPendingSchedule?.schedule}{' '}
|
||||||
<Button
|
<Button
|
||||||
label="Delete"
|
label="Delete"
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteTaskSchedule('download_pending');
|
await deleteTaskSchedule('download_pending');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Automatic video download schedule:</p>
|
<p>Automatic video download schedule:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={downloadPending || downloadPendingSchedule?.schedule || ''}
|
value={downloadPending || downloadPendingSchedule?.schedule || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setDownloadPending(e.currentTarget.value);
|
setDownloadPending(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('download_pending', {
|
await createTaskSchedule('download_pending', {
|
||||||
schedule: downloadPending,
|
schedule: downloadPending,
|
||||||
});
|
});
|
||||||
|
|
||||||
setDownloadPending('');
|
setDownloadPending('');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Refresh Metadata</h2>
|
<h2>Refresh Metadata</h2>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
Current Metadata refresh schedule:{' '}
|
Current Metadata refresh schedule:{' '}
|
||||||
<span className="settings-current">
|
<span className="settings-current">
|
||||||
{!checkReindexSchedule && 'False'}
|
{!checkReindexSchedule && 'False'}
|
||||||
{checkReindexSchedule && (
|
{checkReindexSchedule && (
|
||||||
<>
|
<>
|
||||||
{checkReindexSchedule?.schedule}{' '}
|
{checkReindexSchedule?.schedule}{' '}
|
||||||
<Button
|
<Button
|
||||||
label="Delete"
|
label="Delete"
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteTaskSchedule('check_reindex');
|
await deleteTaskSchedule('check_reindex');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Daily schedule to refresh metadata from YouTube:</p>
|
<p>Daily schedule to refresh metadata from YouTube:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={checkReindex || checkReindexSchedule?.schedule || ''}
|
value={checkReindex || checkReindexSchedule?.schedule || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setCheckReindex(e.currentTarget.value);
|
setCheckReindex(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('check_reindex', {
|
await createTaskSchedule('check_reindex', {
|
||||||
schedule: checkReindex,
|
schedule: checkReindex,
|
||||||
});
|
});
|
||||||
|
|
||||||
setCheckReindex('');
|
setCheckReindex('');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
Current refresh for metadata older than x days:{' '}
|
Current refresh for metadata older than x days:{' '}
|
||||||
<span className="settings-current">{checkReindexSchedule?.config?.days}</span>
|
<span className="settings-current">{checkReindexSchedule?.config?.days}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Refresh older than x days, recommended 90:</p>
|
<p>Refresh older than x days, recommended 90:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={checkReindexDays || checkReindexSchedule?.config?.days || 0}
|
value={checkReindexDays || checkReindexSchedule?.config?.days || 0}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setCheckReindexDays(Number(e.currentTarget.value));
|
setCheckReindexDays(Number(e.currentTarget.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('check_reindex', {
|
await createTaskSchedule('check_reindex', {
|
||||||
config: {
|
config: {
|
||||||
days: checkReindexDays,
|
days: checkReindexDays,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setCheckReindexDays(undefined);
|
setCheckReindexDays(undefined);
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Thumbnail Check</h2>
|
<h2>Thumbnail Check</h2>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
Current thumbnail check schedule:{' '}
|
Current thumbnail check schedule:{' '}
|
||||||
<span className="settings-current">
|
<span className="settings-current">
|
||||||
{!thumbnailCheckSchedule && 'False'}
|
{!thumbnailCheckSchedule && 'False'}
|
||||||
{thumbnailCheckSchedule && (
|
{thumbnailCheckSchedule && (
|
||||||
<>
|
<>
|
||||||
{thumbnailCheckSchedule?.schedule}{' '}
|
{thumbnailCheckSchedule?.schedule}{' '}
|
||||||
<Button
|
<Button
|
||||||
label="Delete"
|
label="Delete"
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteTaskSchedule('thumbnail_check');
|
await deleteTaskSchedule('thumbnail_check');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Periodically check and cleanup thumbnails:</p>
|
<p>Periodically check and cleanup thumbnails:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={thumbnailCheck || thumbnailCheckSchedule?.schedule || ''}
|
value={thumbnailCheck || thumbnailCheckSchedule?.schedule || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setThumbnailCheck(e.currentTarget.value);
|
setThumbnailCheck(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('thumbnail_check', {
|
await createTaskSchedule('thumbnail_check', {
|
||||||
schedule: thumbnailCheck,
|
schedule: thumbnailCheck,
|
||||||
});
|
});
|
||||||
|
|
||||||
setThumbnailCheck('');
|
setThumbnailCheck('');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>ZIP file index backup</h2>
|
<h2>ZIP file index backup</h2>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
<i>
|
<i>
|
||||||
Zip file backups are very slow for large archives and consistency is not guaranteed,
|
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
|
use snapshots instead. Make sure no other tasks are running when creating a Zip file
|
||||||
backup.
|
backup.
|
||||||
</i>
|
</i>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Current index backup schedule:{' '}
|
Current index backup schedule:{' '}
|
||||||
<span className="settings-current">
|
<span className="settings-current">
|
||||||
{!runBackup && 'False'}
|
{!runBackup && 'False'}
|
||||||
{runBackup && (
|
{runBackup && (
|
||||||
<>
|
<>
|
||||||
{runBackup.schedule}{' '}
|
{runBackup.schedule}{' '}
|
||||||
<Button
|
<Button
|
||||||
label="Delete"
|
label="Delete"
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteTaskSchedule('run_backup');
|
await deleteTaskSchedule('run_backup');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Automatically backup metadata to a zip file:</p>
|
<p>Automatically backup metadata to a zip file:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={zipBackup || runBackup?.schedule || ''}
|
value={zipBackup || runBackup?.schedule || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setZipBackup(e.currentTarget.value);
|
setZipBackup(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('run_backup', {
|
await createTaskSchedule('run_backup', {
|
||||||
schedule: zipBackup,
|
schedule: zipBackup,
|
||||||
});
|
});
|
||||||
|
|
||||||
setZipBackup('');
|
setZipBackup('');
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
Current backup files to keep:{' '}
|
Current backup files to keep:{' '}
|
||||||
<span className="settings-current">{runBackup?.config?.rotate}</span>
|
<span className="settings-current">{runBackup?.config?.rotate}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>Max auto backups to keep:</p>
|
<p>Max auto backups to keep:</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={(zipBackupDays || runBackup?.config?.rotate)?.toString() || 0}
|
value={(zipBackupDays || runBackup?.config?.rotate)?.toString() || 0}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setZipBackupDays(Number(e.currentTarget.value));
|
setZipBackupDays(Number(e.currentTarget.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createTaskSchedule('run_backup', {
|
await createTaskSchedule('run_backup', {
|
||||||
config: {
|
config: {
|
||||||
rotate: zipBackupDays,
|
rotate: zipBackupDays,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setZipBackupDays(undefined);
|
setZipBackupDays(undefined);
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>Add Notification URL</h2>
|
<h2>Add Notification URL</h2>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
{!appriseNotification && <p>No notifications stored</p>}
|
{!appriseNotification && <p>No notifications stored</p>}
|
||||||
{appriseNotification && (
|
{appriseNotification && (
|
||||||
<>
|
<>
|
||||||
<div className="description-text">
|
<div className="description-text">
|
||||||
{Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
|
{Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 key={key}>{title}</h3>
|
<h3 key={key}>{title}</h3>
|
||||||
{urls.map((url: string) => {
|
{urls.map((url: string) => {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
<span>{url} </span>
|
<span>{url} </span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="danger-button"
|
className="danger-button"
|
||||||
label="Delete"
|
label="Delete"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
|
await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<p>
|
||||||
<i>
|
<i>
|
||||||
Send notification on completed tasks with the help of the{' '}
|
Send notification on completed tasks with the help of the{' '}
|
||||||
<a href="https://github.com/caronc/apprise" target="_blank">
|
<a href="https://github.com/caronc/apprise" target="_blank">
|
||||||
Apprise
|
Apprise
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
library.
|
library.
|
||||||
</i>
|
</i>
|
||||||
</p>
|
</p>
|
||||||
<select
|
<select
|
||||||
value={notificationTask}
|
value={notificationTask}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setNotificationTask(e.currentTarget.value);
|
setNotificationTask(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">-- select task --</option>
|
<option value="">-- select task --</option>
|
||||||
<option value="update_subscribed">Rescan your Subscriptions</option>
|
<option value="update_subscribed">Rescan your Subscriptions</option>
|
||||||
<option value="extract_download">Add to download queue</option>
|
<option value="extract_download">Add to download queue</option>
|
||||||
<option value="download_pending">Downloading</option>
|
<option value="download_pending">Downloading</option>
|
||||||
<option value="check_reindex">Reindex Documents</option>
|
<option value="check_reindex">Reindex Documents</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Apprise notification URL"
|
placeholder="Apprise notification URL"
|
||||||
value={notificationUrl || ''}
|
value={notificationUrl || ''}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setNotificationUrl(e.currentTarget.value);
|
setNotificationUrl(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createAppriseNotificationUrl(
|
await createAppriseNotificationUrl(
|
||||||
notificationTask as AppriseTaskNameType,
|
notificationTask as AppriseTaskNameType,
|
||||||
notificationUrl || '',
|
notificationUrl || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PaginationDummy />
|
<PaginationDummy />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsScheduling;
|
export default SettingsScheduling;
|
||||||
|
@ -1,126 +1,126 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ColourVariants } from '../api/actions/updateUserConfig';
|
import { ColourVariants } from '../api/actions/updateUserConfig';
|
||||||
import { ColourConstant } from '../configuration/colours/getColours';
|
import { ColourConstant } from '../configuration/colours/getColours';
|
||||||
import SettingsNavigation from '../components/SettingsNavigation';
|
import SettingsNavigation from '../components/SettingsNavigation';
|
||||||
import Notifications from '../components/Notifications';
|
import Notifications from '../components/Notifications';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import loadIsAdmin from '../functions/getIsAdmin';
|
import loadIsAdmin from '../functions/getIsAdmin';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const SettingsUser = () => {
|
const SettingsUser = () => {
|
||||||
const { userConfig, setPartialConfig } = useUserConfigStore();
|
const { userConfig, setPartialConfig } = useUserConfigStore();
|
||||||
const isAdmin = loadIsAdmin();
|
const isAdmin = loadIsAdmin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [styleSheet, setStyleSheet] = useState<ColourVariants>(userConfig.config.stylesheet);
|
const [styleSheet, setStyleSheet] = useState<ColourVariants>(userConfig.config.stylesheet);
|
||||||
const [styleSheetRefresh, setStyleSheetRefresh] = useState(false);
|
const [styleSheetRefresh, setStyleSheetRefresh] = useState(false);
|
||||||
const [pageSize, setPageSize] = useState<number>(userConfig.config.page_size);
|
const [pageSize, setPageSize] = useState<number>(userConfig.config.page_size);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setStyleSheet(userConfig.config.stylesheet);
|
setStyleSheet(userConfig.config.stylesheet);
|
||||||
setPageSize(userConfig.config.page_size);
|
setPageSize(userConfig.config.page_size);
|
||||||
})();
|
})();
|
||||||
}, [userConfig.config.page_size, userConfig.config.stylesheet]);
|
}, [userConfig.config.page_size, userConfig.config.stylesheet]);
|
||||||
|
|
||||||
const handleStyleSheetChange = async (selectedStyleSheet: ColourVariants) => {
|
const handleStyleSheetChange = async (selectedStyleSheet: ColourVariants) => {
|
||||||
setPartialConfig({stylesheet: selectedStyleSheet});
|
setPartialConfig({ stylesheet: selectedStyleSheet });
|
||||||
setStyleSheet(selectedStyleSheet);
|
setStyleSheet(selectedStyleSheet);
|
||||||
setStyleSheetRefresh(true);
|
setStyleSheetRefresh(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = async () => {
|
const handlePageSizeChange = async () => {
|
||||||
setPartialConfig({page_size: pageSize});
|
setPartialConfig({ page_size: pageSize });
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePageRefresh = () => {
|
const handlePageRefresh = () => {
|
||||||
navigate(0);
|
navigate(0);
|
||||||
setStyleSheetRefresh(false);
|
setStyleSheetRefresh(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>TA | User Settings</title>
|
<title>TA | User Settings</title>
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<SettingsNavigation />
|
<SettingsNavigation />
|
||||||
<Notifications pageName={'all'} />
|
<Notifications pageName={'all'} />
|
||||||
|
|
||||||
<div className="title-bar">
|
<div className="title-bar">
|
||||||
<h1>User Configurations</h1>
|
<h1>User Configurations</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='info-box'>
|
<div className="info-box">
|
||||||
<div className='info-box-item'>
|
<div className="info-box-item">
|
||||||
<h2>Customize user Interface</h2>
|
<h2>Customize user Interface</h2>
|
||||||
<div className='settings-box-wrapper'>
|
<div className="settings-box-wrapper">
|
||||||
<div>
|
<div>
|
||||||
<p>Switch your color scheme</p>
|
<p>Switch your color scheme</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<select
|
<select
|
||||||
name="stylesheet"
|
name="stylesheet"
|
||||||
id="id_stylesheet"
|
id="id_stylesheet"
|
||||||
value={styleSheet}
|
value={styleSheet}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
handleStyleSheetChange(event.target.value as ColourVariants);
|
handleStyleSheetChange(event.target.value as ColourVariants);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.entries(ColourConstant).map(([key, value]) => {
|
{Object.entries(ColourConstant).map(([key, value]) => {
|
||||||
return (
|
return (
|
||||||
<option key={key} value={value}>
|
<option key={key} value={value}>
|
||||||
{key}
|
{key}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
{styleSheetRefresh && (
|
{styleSheetRefresh && <button onClick={handlePageRefresh}>Refresh</button>}
|
||||||
<button onClick={handlePageRefresh}>Refresh</button>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="settings-box-wrapper">
|
||||||
</div>
|
<div>
|
||||||
<div className='settings-box-wrapper'>
|
<p>Archive view page size</p>
|
||||||
<div>
|
</div>
|
||||||
<p>Archive view page size</p>
|
<div>
|
||||||
</div>
|
<input
|
||||||
<div>
|
type="number"
|
||||||
<input
|
name="page_size"
|
||||||
type="number"
|
id="id_page_size"
|
||||||
name="page_size"
|
value={pageSize || 12}
|
||||||
id="id_page_size"
|
onChange={event => {
|
||||||
value={pageSize || 12}
|
setPageSize(Number(event.target.value));
|
||||||
onChange={event => {
|
}}
|
||||||
setPageSize(Number(event.target.value));
|
/>
|
||||||
}}
|
<div className="button-box">
|
||||||
/>
|
{userConfig.config.page_size !== pageSize && (
|
||||||
<div className='button-box'>
|
<>
|
||||||
{userConfig.config.page_size !== pageSize && (
|
<button onClick={handlePageSizeChange}>Update</button>
|
||||||
<>
|
<button onClick={() => setPageSize(userConfig.config.page_size)}>
|
||||||
<button onClick={handlePageSizeChange}>Update</button>
|
Cancel
|
||||||
<button onClick={() => setPageSize(userConfig.config.page_size)}>Cancel</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
<h2>User Management</h2>
|
<h2>User Management</h2>
|
||||||
<p>
|
<p>
|
||||||
Access the admin interface for basic user management functionality like adding and
|
Access the admin interface for basic user management functionality like adding and
|
||||||
deleting users, changing passwords and more.
|
deleting users, changing passwords and more.
|
||||||
</p>
|
</p>
|
||||||
<a href="/admin/">
|
<a href="/admin/">
|
||||||
<Button label="Admin Interface" />
|
<Button label="Admin Interface" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsUser;
|
export default SettingsUser;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ interface AuthState {
|
|||||||
setAuth: (auth: AuthenticationType) => void;
|
setAuth: (auth: AuthenticationType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
export const useAuthStore = create<AuthState>(set => ({
|
||||||
auth: null,
|
auth: null,
|
||||||
setAuth: (auth) => set({ auth }),
|
setAuth: auth => set({ auth }),
|
||||||
}));
|
}));
|
||||||
|
@ -7,8 +7,7 @@ interface UserConfigState {
|
|||||||
setPartialConfig: (userConfig: Partial<UserConfigType>) => void;
|
setPartialConfig: (userConfig: Partial<UserConfigType>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserConfigStore = create<UserConfigState>((set) => ({
|
export const useUserConfigStore = create<UserConfigState>(set => ({
|
||||||
|
|
||||||
userConfig: {
|
userConfig: {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: '',
|
name: '',
|
||||||
@ -30,15 +29,16 @@ export const useUserConfigStore = create<UserConfigState>((set) => ({
|
|||||||
hide_watched: false,
|
hide_watched: false,
|
||||||
show_ignored_only: false,
|
show_ignored_only: false,
|
||||||
show_subed_only: false,
|
show_subed_only: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setUserConfig: (userConfig) => set({ userConfig }),
|
setUserConfig: userConfig => set({ userConfig }),
|
||||||
|
|
||||||
setPartialConfig: async (userConfig: Partial<UserConfigType>) => {
|
setPartialConfig: async (userConfig: Partial<UserConfigType>) => {
|
||||||
const userConfigResponse = await updateUserConfig(userConfig);
|
const userConfigResponse = await updateUserConfig(userConfig);
|
||||||
set((state) => ({
|
set(state => ({
|
||||||
userConfig: state.userConfig ? { ...state.userConfig, config: userConfigResponse } : state.userConfig,
|
userConfig: state.userConfig
|
||||||
|
? { ...state.userConfig, config: userConfigResponse }
|
||||||
|
: state.userConfig,
|
||||||
}));
|
}));
|
||||||
}
|
},
|
||||||
|
}));
|
||||||
}))
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user