diff --git a/backend/common/src/helper.py b/backend/common/src/helper.py
index 255adcc1..a7db4f3e 100644
--- a/backend/common/src/helper.py
+++ b/backend/common/src/helper.py
@@ -1,269 +1,261 @@
-"""
-Loose collection of helper functions
-- don't import AppConfig class here to avoid circular imports
-"""
-
-import json
-import os
-import random
-import string
-import subprocess
-from datetime import datetime
-from typing import Any
-from urllib.parse import urlparse
-
-import requests
-from common.src.env_settings import EnvironmentSettings
-from common.src.es_connect import IndexPaginate
-
-
-def ignore_filelist(filelist: list[str]) -> list[str]:
- """ignore temp files for os.listdir sanitizer"""
- to_ignore = [
- "@eaDir",
- "Icon\r\r",
- "Network Trash Folder",
- "Temporary Items",
- ]
- cleaned: list[str] = []
- for file_name in filelist:
- if file_name.startswith(".") or file_name in to_ignore:
- continue
-
- cleaned.append(file_name)
-
- return cleaned
-
-
-def randomizor(length: int) -> str:
- """generate random alpha numeric string"""
- pool: str = string.digits + string.ascii_letters
- return "".join(random.choice(pool) for i in range(length))
-
-
-def requests_headers() -> dict[str, str]:
- """build header with random user agent for requests outside of yt-dlp"""
-
- chrome_versions = (
- "90.0.4430.212",
- "90.0.4430.24",
- "90.0.4430.70",
- "90.0.4430.72",
- "90.0.4430.85",
- "90.0.4430.93",
- "91.0.4472.101",
- "91.0.4472.106",
- "91.0.4472.114",
- "91.0.4472.124",
- "91.0.4472.164",
- "91.0.4472.19",
- "91.0.4472.77",
- "92.0.4515.107",
- "92.0.4515.115",
- "92.0.4515.131",
- "92.0.4515.159",
- "92.0.4515.43",
- "93.0.4556.0",
- "93.0.4577.15",
- "93.0.4577.63",
- "93.0.4577.82",
- "94.0.4606.41",
- "94.0.4606.54",
- "94.0.4606.61",
- "94.0.4606.71",
- "94.0.4606.81",
- "94.0.4606.85",
- "95.0.4638.17",
- "95.0.4638.50",
- "95.0.4638.54",
- "95.0.4638.69",
- "95.0.4638.74",
- "96.0.4664.18",
- "96.0.4664.45",
- "96.0.4664.55",
- "96.0.4664.93",
- "97.0.4692.20",
- )
- template = (
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
- + "AppleWebKit/537.36 (KHTML, like Gecko) "
- + f"Chrome/{random.choice(chrome_versions)} Safari/537.36"
- )
-
- return {"User-Agent": template}
-
-
-def date_parser(timestamp: int | str) -> str:
- """return formatted date string"""
- if isinstance(timestamp, int):
- date_obj = datetime.fromtimestamp(timestamp)
- elif isinstance(timestamp, str):
- date_obj = datetime.strptime(timestamp, "%Y-%m-%d")
- else:
- raise TypeError(f"invalid timestamp: {timestamp}")
-
- return date_obj.date().isoformat()
-
-
-def time_parser(timestamp: str) -> float:
- """return seconds from timestamp, false on empty"""
- if not timestamp:
- return False
-
- if timestamp.isnumeric():
- return int(timestamp)
-
- hours, minutes, seconds = timestamp.split(":", maxsplit=3)
- return int(hours) * 60 * 60 + int(minutes) * 60 + float(seconds)
-
-
-def clear_dl_cache(cache_dir: str) -> int:
- """clear leftover files from dl cache"""
- print("clear download cache")
- download_cache_dir = os.path.join(cache_dir, "download")
- leftover_files = ignore_filelist(os.listdir(download_cache_dir))
- for cached in leftover_files:
- to_delete = os.path.join(download_cache_dir, cached)
- os.remove(to_delete)
-
- return len(leftover_files)
-
-
-def get_mapping() -> dict:
- """read index_mapping.json and get expected mapping and settings"""
- with open("appsettings/index_mapping.json", "r", encoding="utf-8") as f:
- index_config: dict = json.load(f).get("index_config")
-
- return index_config
-
-
-def is_shorts(youtube_id: str) -> bool:
- """check if youtube_id is a shorts video, bot not it it's not a shorts"""
- shorts_url = f"https://www.youtube.com/shorts/{youtube_id}"
- cookies = {"SOCS": "CAI"}
- response = requests.head(
- shorts_url, cookies=cookies, headers=requests_headers(), timeout=10
- )
-
- return response.status_code == 200
-
-
-def get_duration_sec(file_path: str) -> int:
- """get duration of media file from file path"""
-
- duration = subprocess.run(
- [
- "ffprobe",
- "-v",
- "error",
- "-show_entries",
- "format=duration",
- "-of",
- "default=noprint_wrappers=1:nokey=1",
- file_path,
- ],
- capture_output=True,
- check=True,
- )
- duration_raw = duration.stdout.decode().strip()
- if duration_raw == "N/A":
- return 0
-
- duration_sec = int(float(duration_raw))
- return duration_sec
-
-
-def get_duration_str(seconds: int) -> str:
- """Return a human-readable duration string from seconds."""
- if not seconds:
- return "NA"
-
- units = [("y", 31536000), ("d", 86400), ("h", 3600), ("m", 60), ("s", 1)]
- duration_parts = []
-
- for unit_label, unit_seconds in units:
- if seconds >= unit_seconds:
- unit_count, seconds = divmod(seconds, unit_seconds)
- duration_parts.append(f"{unit_count:02}{unit_label}")
-
- duration_parts[0] = duration_parts[0].lstrip("0")
-
- return " ".join(duration_parts)
-
-
-def ta_host_parser(ta_host: str) -> tuple[list[str], list[str]]:
- """parse ta_host env var for ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS"""
- allowed_hosts: list[str] = [
- "localhost",
- "tubearchivist",
- ]
- csrf_trusted_origins: list[str] = [
- "http://localhost",
- "http://tubearchivist",
- ]
- for host in ta_host.split():
- host_clean = host.strip()
- if not host_clean.startswith("http"):
- host_clean = f"http://{host_clean}"
-
- parsed = urlparse(host_clean)
- allowed_hosts.append(f"{parsed.hostname}")
- csrf_trusted_origins.append(f"{parsed.scheme}://{parsed.hostname}")
-
- return allowed_hosts, csrf_trusted_origins
-
-
-def get_stylesheets() -> list:
- """Get all valid stylesheets from /static/css"""
- app_root = EnvironmentSettings.APP_DIR
- try:
- stylesheets = os.listdir(os.path.join(app_root, "static/css"))
- except FileNotFoundError:
- return []
-
- stylesheets.remove("style.css")
- stylesheets.sort()
- stylesheets = list(filter(lambda x: x.endswith(".css"), stylesheets))
- return stylesheets
-
-
-def check_stylesheet(stylesheet: str):
- """Check if a stylesheet exists. Return dark.css as a fallback"""
- if stylesheet in get_stylesheets():
- return stylesheet
-
- return "dark.css"
-
-
-def is_missing(
- to_check: str | list[str],
- index_name: str = "ta_video,ta_download",
- on_key: str = "youtube_id",
-) -> list[str]:
- """id or list of ids that are missing from index_name"""
- if isinstance(to_check, str):
- to_check = [to_check]
-
- data = {
- "query": {"terms": {on_key: to_check}},
- "_source": [on_key],
- }
- result = IndexPaginate(index_name, data=data).get_results()
- existing_ids = [i[on_key] for i in result]
- dl = [i for i in to_check if i not in existing_ids]
-
- return dl
-
-
-def get_channel_overwrites() -> dict[str, dict[str, Any]]:
- """get overwrites indexed my channel_id"""
- data = {
- "query": {
- "bool": {"must": [{"exists": {"field": "channel_overwrites"}}]}
- },
- "_source": ["channel_id", "channel_overwrites"],
- }
- result = IndexPaginate("ta_channel", data).get_results()
- overwrites = {i["channel_id"]: i["channel_overwrites"] for i in result}
-
- return overwrites
+"""
+Loose collection of helper functions
+- don't import AppConfig class here to avoid circular imports
+"""
+
+import json
+import os
+import random
+import string
+import subprocess
+from datetime import datetime
+from typing import Any
+from urllib.parse import urlparse
+
+import requests
+from common.src.es_connect import IndexPaginate
+
+
+def ignore_filelist(filelist: list[str]) -> list[str]:
+ """ignore temp files for os.listdir sanitizer"""
+ to_ignore = [
+ "@eaDir",
+ "Icon\r\r",
+ "Network Trash Folder",
+ "Temporary Items",
+ ]
+ cleaned: list[str] = []
+ for file_name in filelist:
+ if file_name.startswith(".") or file_name in to_ignore:
+ continue
+
+ cleaned.append(file_name)
+
+ return cleaned
+
+
+def randomizor(length: int) -> str:
+ """generate random alpha numeric string"""
+ pool: str = string.digits + string.ascii_letters
+ return "".join(random.choice(pool) for i in range(length))
+
+
+def requests_headers() -> dict[str, str]:
+ """build header with random user agent for requests outside of yt-dlp"""
+
+ chrome_versions = (
+ "90.0.4430.212",
+ "90.0.4430.24",
+ "90.0.4430.70",
+ "90.0.4430.72",
+ "90.0.4430.85",
+ "90.0.4430.93",
+ "91.0.4472.101",
+ "91.0.4472.106",
+ "91.0.4472.114",
+ "91.0.4472.124",
+ "91.0.4472.164",
+ "91.0.4472.19",
+ "91.0.4472.77",
+ "92.0.4515.107",
+ "92.0.4515.115",
+ "92.0.4515.131",
+ "92.0.4515.159",
+ "92.0.4515.43",
+ "93.0.4556.0",
+ "93.0.4577.15",
+ "93.0.4577.63",
+ "93.0.4577.82",
+ "94.0.4606.41",
+ "94.0.4606.54",
+ "94.0.4606.61",
+ "94.0.4606.71",
+ "94.0.4606.81",
+ "94.0.4606.85",
+ "95.0.4638.17",
+ "95.0.4638.50",
+ "95.0.4638.54",
+ "95.0.4638.69",
+ "95.0.4638.74",
+ "96.0.4664.18",
+ "96.0.4664.45",
+ "96.0.4664.55",
+ "96.0.4664.93",
+ "97.0.4692.20",
+ )
+ template = (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ + "AppleWebKit/537.36 (KHTML, like Gecko) "
+ + f"Chrome/{random.choice(chrome_versions)} Safari/537.36"
+ )
+
+ return {"User-Agent": template}
+
+
+def date_parser(timestamp: int | str) -> str:
+ """return formatted date string"""
+ if isinstance(timestamp, int):
+ date_obj = datetime.fromtimestamp(timestamp)
+ elif isinstance(timestamp, str):
+ date_obj = datetime.strptime(timestamp, "%Y-%m-%d")
+ else:
+ raise TypeError(f"invalid timestamp: {timestamp}")
+
+ return date_obj.date().isoformat()
+
+
+def time_parser(timestamp: str) -> float:
+ """return seconds from timestamp, false on empty"""
+ if not timestamp:
+ return False
+
+ if timestamp.isnumeric():
+ return int(timestamp)
+
+ hours, minutes, seconds = timestamp.split(":", maxsplit=3)
+ return int(hours) * 60 * 60 + int(minutes) * 60 + float(seconds)
+
+
+def clear_dl_cache(cache_dir: str) -> int:
+ """clear leftover files from dl cache"""
+ print("clear download cache")
+ download_cache_dir = os.path.join(cache_dir, "download")
+ leftover_files = ignore_filelist(os.listdir(download_cache_dir))
+ for cached in leftover_files:
+ to_delete = os.path.join(download_cache_dir, cached)
+ os.remove(to_delete)
+
+ return len(leftover_files)
+
+
+def get_mapping() -> dict:
+ """read index_mapping.json and get expected mapping and settings"""
+ with open("appsettings/index_mapping.json", "r", encoding="utf-8") as f:
+ index_config: dict = json.load(f).get("index_config")
+
+ return index_config
+
+
+def is_shorts(youtube_id: str) -> bool:
+ """check if youtube_id is a shorts video, bot not it it's not a shorts"""
+ shorts_url = f"https://www.youtube.com/shorts/{youtube_id}"
+ cookies = {"SOCS": "CAI"}
+ response = requests.head(
+ shorts_url, cookies=cookies, headers=requests_headers(), timeout=10
+ )
+
+ return response.status_code == 200
+
+
+def get_duration_sec(file_path: str) -> int:
+ """get duration of media file from file path"""
+
+ duration = subprocess.run(
+ [
+ "ffprobe",
+ "-v",
+ "error",
+ "-show_entries",
+ "format=duration",
+ "-of",
+ "default=noprint_wrappers=1:nokey=1",
+ file_path,
+ ],
+ capture_output=True,
+ check=True,
+ )
+ duration_raw = duration.stdout.decode().strip()
+ if duration_raw == "N/A":
+ return 0
+
+ duration_sec = int(float(duration_raw))
+ return duration_sec
+
+
+def get_duration_str(seconds: int) -> str:
+ """Return a human-readable duration string from seconds."""
+ if not seconds:
+ return "NA"
+
+ units = [("y", 31536000), ("d", 86400), ("h", 3600), ("m", 60), ("s", 1)]
+ duration_parts = []
+
+ for unit_label, unit_seconds in units:
+ if seconds >= unit_seconds:
+ unit_count, seconds = divmod(seconds, unit_seconds)
+ duration_parts.append(f"{unit_count:02}{unit_label}")
+
+ duration_parts[0] = duration_parts[0].lstrip("0")
+
+ return " ".join(duration_parts)
+
+
+def ta_host_parser(ta_host: str) -> tuple[list[str], list[str]]:
+ """parse ta_host env var for ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS"""
+ allowed_hosts: list[str] = [
+ "localhost",
+ "tubearchivist",
+ ]
+ csrf_trusted_origins: list[str] = [
+ "http://localhost",
+ "http://tubearchivist",
+ ]
+ for host in ta_host.split():
+ host_clean = host.strip()
+ if not host_clean.startswith("http"):
+ host_clean = f"http://{host_clean}"
+
+ parsed = urlparse(host_clean)
+ allowed_hosts.append(f"{parsed.hostname}")
+ csrf_trusted_origins.append(f"{parsed.scheme}://{parsed.hostname}")
+
+ return allowed_hosts, csrf_trusted_origins
+
+
+def get_stylesheets() -> list:
+ """Get all valid stylesheets from /static/css"""
+
+ stylesheets = ["dark.css", "light.css", "matrix.css", "midnight.css"]
+ return stylesheets
+
+
+def check_stylesheet(stylesheet: str):
+ """Check if a stylesheet exists. Return dark.css as a fallback"""
+ if stylesheet in get_stylesheets():
+ return stylesheet
+
+ return "dark.css"
+
+
+def is_missing(
+ to_check: str | list[str],
+ index_name: str = "ta_video,ta_download",
+ on_key: str = "youtube_id",
+) -> list[str]:
+ """id or list of ids that are missing from index_name"""
+ if isinstance(to_check, str):
+ to_check = [to_check]
+
+ data = {
+ "query": {"terms": {on_key: to_check}},
+ "_source": [on_key],
+ }
+ result = IndexPaginate(index_name, data=data).get_results()
+ existing_ids = [i[on_key] for i in result]
+ dl = [i for i in to_check if i not in existing_ids]
+
+ return dl
+
+
+def get_channel_overwrites() -> dict[str, dict[str, Any]]:
+ """get overwrites indexed my channel_id"""
+ data = {
+ "query": {
+ "bool": {"must": [{"exists": {"field": "channel_overwrites"}}]}
+ },
+ "_source": ["channel_id", "channel_overwrites"],
+ }
+ result = IndexPaginate("ta_channel", data).get_results()
+ overwrites = {i["channel_id"]: i["channel_overwrites"] for i in result}
+
+ return overwrites
diff --git a/frontend/index.html b/frontend/index.html
index 6396d71a..7bf3d7ad 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,26 +1,26 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TubeArchivist
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TubeArchivist
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index d8d9a818..be7414cf 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,2961 +1,2923 @@
-{
- "name": "tubearchivist-frontend",
- "version": "0.1.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "tubearchivist-frontend",
- "version": "0.1.0",
- "dependencies": {
- "dompurify": "^3.1.6",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-helmet": "^6.1.0",
- "react-router-dom": "^6.25.1"
- },
- "devDependencies": {
- "@types/dompurify": "^3.0.5",
- "@types/react": "^18.3.3",
- "@types/react-dom": "^18.3.0",
- "@types/react-helmet": "^6.1.11",
- "@typescript-eslint/eslint-plugin": "^7.16.1",
- "@typescript-eslint/parser": "^7.16.1",
- "@vitejs/plugin-react-swc": "^3.7.0",
- "eslint": "^8.57.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-plugin-react-hooks": "^4.6.2",
- "eslint-plugin-react-refresh": "^0.4.8",
- "prettier": "3.3.3",
- "typescript": "^5.5.3",
- "vite": "^5.3.4"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
- "dev": true,
- "dependencies": {
- "eslint-visitor-keys": "^3.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
- "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
- "dev": true,
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
- "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
- "dev": true,
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^9.6.0",
- "globals": "^13.19.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/@eslint/js": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
- "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
- "dev": true,
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- }
- },
- "node_modules/@humanwhocodes/config-array": {
- "version": "0.11.14",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
- "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
- "dev": true,
- "dependencies": {
- "@humanwhocodes/object-schema": "^2.0.2",
- "debug": "^4.3.1",
- "minimatch": "^3.0.5"
- },
- "engines": {
- "node": ">=10.10.0"
- }
- },
- "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
- "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
- "dev": true
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@remix-run/router": {
- "version": "1.18.0",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz",
- "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==",
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz",
- "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz",
- "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz",
- "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz",
- "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz",
- "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz",
- "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz",
- "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz",
- "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz",
- "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz",
- "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz",
- "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz",
- "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz",
- "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz",
- "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz",
- "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz",
- "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@swc/core": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz",
- "integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@swc/counter": "^0.1.3",
- "@swc/types": "^0.1.9"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/swc"
- },
- "optionalDependencies": {
- "@swc/core-darwin-arm64": "1.6.13",
- "@swc/core-darwin-x64": "1.6.13",
- "@swc/core-linux-arm-gnueabihf": "1.6.13",
- "@swc/core-linux-arm64-gnu": "1.6.13",
- "@swc/core-linux-arm64-musl": "1.6.13",
- "@swc/core-linux-x64-gnu": "1.6.13",
- "@swc/core-linux-x64-musl": "1.6.13",
- "@swc/core-win32-arm64-msvc": "1.6.13",
- "@swc/core-win32-ia32-msvc": "1.6.13",
- "@swc/core-win32-x64-msvc": "1.6.13"
- },
- "peerDependencies": {
- "@swc/helpers": "*"
- },
- "peerDependenciesMeta": {
- "@swc/helpers": {
- "optional": true
- }
- }
- },
- "node_modules/@swc/core-darwin-arm64": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz",
- "integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-darwin-x64": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz",
- "integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-arm-gnueabihf": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz",
- "integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-arm64-gnu": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz",
- "integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-arm64-musl": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz",
- "integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-x64-gnu": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz",
- "integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-x64-musl": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz",
- "integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-win32-arm64-msvc": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz",
- "integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-win32-ia32-msvc": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz",
- "integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-win32-x64-msvc": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz",
- "integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/counter": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
- "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@swc/types": {
- "version": "0.1.9",
- "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz",
- "integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@swc/counter": "^0.1.3"
- }
- },
- "node_modules/@types/dompurify": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
- "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
- "dev": true,
- "dependencies": {
- "@types/trusted-types": "*"
- }
- },
- "node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
- "dev": true
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.12",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
- "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
- "dev": true
- },
- "node_modules/@types/react": {
- "version": "18.3.3",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
- "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "18.3.0",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
- "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
- "dev": true,
- "dependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@types/react-helmet": {
- "version": "6.1.11",
- "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz",
- "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==",
- "dev": true,
- "dependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@types/trusted-types": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "dev": true
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz",
- "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "7.16.1",
- "@typescript-eslint/type-utils": "7.16.1",
- "@typescript-eslint/utils": "7.16.1",
- "@typescript-eslint/visitor-keys": "7.16.1",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^7.0.0",
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/parser": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz",
- "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "@typescript-eslint/scope-manager": "7.16.1",
- "@typescript-eslint/types": "7.16.1",
- "@typescript-eslint/typescript-estree": "7.16.1",
- "@typescript-eslint/visitor-keys": "7.16.1",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz",
- "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "7.16.1",
- "@typescript-eslint/visitor-keys": "7.16.1"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/type-utils": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz",
- "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/typescript-estree": "7.16.1",
- "@typescript-eslint/utils": "7.16.1",
- "debug": "^4.3.4",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz",
- "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz",
- "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "@typescript-eslint/types": "7.16.1",
- "@typescript-eslint/visitor-keys": "7.16.1",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/utils": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz",
- "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "7.16.1",
- "@typescript-eslint/types": "7.16.1",
- "@typescript-eslint/typescript-estree": "7.16.1"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.56.0"
- }
- },
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.16.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz",
- "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "7.16.1",
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true
- },
- "node_modules/@vitejs/plugin-react-swc": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz",
- "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@swc/core": "^1.5.7"
- },
- "peerDependencies": {
- "vite": "^4 || ^5"
- }
- },
- "node_modules/acorn": {
- "version": "8.11.3",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
- "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
- "dev": true,
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
- },
- "node_modules/array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
- },
- "node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
- },
- "node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true
- },
- "node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true
- },
- "node_modules/dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/dompurify": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
- "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
- "license": "(MPL-2.0 OR Apache-2.0)"
- },
- "node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
- }
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
- "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
- "dev": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.57.0",
- "@humanwhocodes/config-array": "^0.11.14",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
- "debug": "^4.3.2",
- "doctrine": "^3.0.0",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.2",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1",
- "esquery": "^1.4.2",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "globals": "^13.19.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3",
- "strip-ansi": "^6.0.1",
- "text-table": "^0.2.0"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-config-prettier": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
- "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
- "dev": true,
- "bin": {
- "eslint-config-prettier": "bin/cli.js"
- },
- "peerDependencies": {
- "eslint": ">=7.0.0"
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
- "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
- }
- },
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.8",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.8.tgz",
- "integrity": "sha512-MIKAclwaDFIiYtVBLzDdm16E+Ty4GwhB6wZlCAG1R3Ur+F9Qbo6PRxpA5DK7XtDgm+WlCoAY2WxAwqhmIDHg6Q==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "eslint": ">=7"
- }
- },
- "node_modules/eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
- "dev": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/eslint/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
- "dev": true,
- "dependencies": {
- "acorn": "^8.9.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
- "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
- "dev": true,
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
- },
- "node_modules/fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true
- },
- "node_modules/fastq": {
- "version": "1.17.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
- "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/file-entry-cache": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
- "dev": true,
- "dependencies": {
- "flat-cache": "^3.0.4"
- },
- "engines": {
- "node": "^10.12.0 || >=12.0.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
- "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
- "dev": true,
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.3",
- "rimraf": "^3.0.2"
- },
- "engines": {
- "node": "^10.12.0 || >=12.0.0"
- }
- },
- "node_modules/flatted": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
- "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
- "dev": true
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/globals": {
- "version": "13.24.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
- "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
- "dev": true,
- "dependencies": {
- "type-fest": "^0.20.2"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ignore": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
- "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
- "dev": true,
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/is-path-inside": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
- "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true
- },
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
- "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/picocolors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
- "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.4.39",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
- "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.0.1",
- "source-map-js": "^1.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/prettier": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
- "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "prettier": "bin/prettier.cjs"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
- }
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
- "node_modules/react-fast-compare": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
- "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
- },
- "node_modules/react-helmet": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
- "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
- "dependencies": {
- "object-assign": "^4.1.1",
- "prop-types": "^15.7.2",
- "react-fast-compare": "^3.1.1",
- "react-side-effect": "^2.1.0"
- },
- "peerDependencies": {
- "react": ">=16.3.0"
- }
- },
- "node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
- },
- "node_modules/react-router": {
- "version": "6.25.1",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz",
- "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/router": "1.18.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8"
- }
- },
- "node_modules/react-router-dom": {
- "version": "6.25.1",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz",
- "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/router": "1.18.0",
- "react-router": "6.25.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8",
- "react-dom": ">=16.8"
- }
- },
- "node_modules/react-side-effect": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
- "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
- "peerDependencies": {
- "react": "^16.3.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rollup": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
- "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
- "dev": true,
- "dependencies": {
- "@types/estree": "1.0.5"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.17.2",
- "@rollup/rollup-android-arm64": "4.17.2",
- "@rollup/rollup-darwin-arm64": "4.17.2",
- "@rollup/rollup-darwin-x64": "4.17.2",
- "@rollup/rollup-linux-arm-gnueabihf": "4.17.2",
- "@rollup/rollup-linux-arm-musleabihf": "4.17.2",
- "@rollup/rollup-linux-arm64-gnu": "4.17.2",
- "@rollup/rollup-linux-arm64-musl": "4.17.2",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2",
- "@rollup/rollup-linux-riscv64-gnu": "4.17.2",
- "@rollup/rollup-linux-s390x-gnu": "4.17.2",
- "@rollup/rollup-linux-x64-gnu": "4.17.2",
- "@rollup/rollup-linux-x64-musl": "4.17.2",
- "@rollup/rollup-win32-arm64-msvc": "4.17.2",
- "@rollup/rollup-win32-ia32-msvc": "4.17.2",
- "@rollup/rollup-win32-x64-msvc": "4.17.2",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
- },
- "node_modules/semver": {
- "version": "7.6.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
- "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
- "dev": true
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/ts-api-utils": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
- "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
- "dev": true,
- "engines": {
- "node": ">=16"
- },
- "peerDependencies": {
- "typescript": ">=4.2.0"
- }
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/typescript": {
- "version": "5.5.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
- "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/vite": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
- "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.39",
- "rollup": "^4.13.0"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- }
- }
-}
+{
+ "name": "tubearchivist-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "tubearchivist-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "dompurify": "^3.2.3",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.0.2"
+ },
+ "devDependencies": {
+ "@types/react": "^19.0.1",
+ "@types/react-dom": "^19.0.2",
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
+ "@typescript-eslint/parser": "^8.18.0",
+ "@vitejs/plugin-react-swc": "^3.7.2",
+ "eslint": "^9.16.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "prettier": "3.4.2",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.3"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
+ "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
+ "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
+ "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
+ "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
+ "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
+ "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
+ "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
+ "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
+ "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
+ "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
+ "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
+ "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
+ "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
+ "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
+ "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
+ "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
+ "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
+ "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
+ "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
+ "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
+ "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
+ "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
+ "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
+ "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
+ "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.5",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
+ "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+ "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.16.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
+ "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
+ "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
+ "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+ "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
+ "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
+ "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
+ "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
+ "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
+ "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
+ "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
+ "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
+ "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
+ "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
+ "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
+ "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
+ "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
+ "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
+ "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
+ "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
+ "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
+ "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
+ "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
+ "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-+CuuTCmQFfzaNGg1JmcZvdUVITQXJk9sMnl1C2TiDLzOSVOJRwVD4dNo5dljX/qxpMAN+2BIYlwjlSkoGi6grg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.17"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.10.0",
+ "@swc/core-darwin-x64": "1.10.0",
+ "@swc/core-linux-arm-gnueabihf": "1.10.0",
+ "@swc/core-linux-arm64-gnu": "1.10.0",
+ "@swc/core-linux-arm64-musl": "1.10.0",
+ "@swc/core-linux-x64-gnu": "1.10.0",
+ "@swc/core-linux-x64-musl": "1.10.0",
+ "@swc/core-win32-arm64-msvc": "1.10.0",
+ "@swc/core-win32-ia32-msvc": "1.10.0",
+ "@swc/core-win32-x64-msvc": "1.10.0"
+ },
+ "peerDependencies": {
+ "@swc/helpers": "*"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.0.tgz",
+ "integrity": "sha512-wCeUpanqZyzvgqWRtXIyhcFK3CqukAlYyP+fJpY2gWc/+ekdrenNIfZMwY7tyTFDkXDYEKzvn3BN/zDYNJFowQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.0.tgz",
+ "integrity": "sha512-0CZPzqTynUBO+SHEl/qKsFSahp2Jv/P2ZRjFG0gwZY5qIcr1+B/v+o74/GyNMBGz9rft+F2WpU31gz2sJwyF4A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.0.tgz",
+ "integrity": "sha512-oq+DdMu5uJOFPtRkeiITc4kxmd+QSmK+v+OBzlhdGkSgoH3yRWZP+H2ao0cBXo93ZgCr2LfjiER0CqSKhjGuNA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.0.tgz",
+ "integrity": "sha512-Y6+PC8knchEViRxiCUj3j8wsGXaIhuvU+WqrFqV834eiItEMEI9+Vh3FovqJMBE3L7d4E4ZQtgImHCXjrHfxbw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.0.tgz",
+ "integrity": "sha512-EbrX9A5U4cECCQQfky7945AW9GYnTXtCUXElWTkTYmmyQK87yCyFfY8hmZ9qMFIwxPOH6I3I2JwMhzdi8Qoz7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.0.tgz",
+ "integrity": "sha512-TaxpO6snTjjfLXFYh5EjZ78se69j2gDcqEM8yB9gguPYwkCHi2Ylfmh7iVaNADnDJFtjoAQp0L41bTV/Pfq9Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.0.tgz",
+ "integrity": "sha512-IEGvDd6aEEKEyZFZ8oCKuik05G5BS7qwG5hO5PEMzdGeh8JyFZXxsfFXbfeAqjue4UaUUrhnoX+Ze3M2jBVMHw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.0.tgz",
+ "integrity": "sha512-UkQ952GSpY+Z6XONj9GSW8xGSkF53jrCsuLj0nrcuw7Dvr1a816U/9WYZmmcYS8tnG2vHylhpm6csQkyS8lpCw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.0.tgz",
+ "integrity": "sha512-a2QpIZmTiT885u/mUInpeN2W9ClCnqrV2LnMqJR1/Fgx1Afw/hAtiDZPtQ0SqS8yDJ2VR5gfNZo3gpxWMrqdVA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.0.tgz",
+ "integrity": "sha512-tZcCmMwf483nwsEBfUk5w9e046kMa1iSik4bP9Kwi2FGtOfHuDfIcwW4jek3hdcgF5SaBW1ktnK/lgQLDi5AtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.17",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
+ "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.0.1",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz",
+ "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.0.2",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
+ "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz",
+ "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.18.0",
+ "@typescript-eslint/type-utils": "8.18.0",
+ "@typescript-eslint/utils": "8.18.0",
+ "@typescript-eslint/visitor-keys": "8.18.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz",
+ "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==",
+ "dev": true,
+ "license": "MITClause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.18.0",
+ "@typescript-eslint/types": "8.18.0",
+ "@typescript-eslint/typescript-estree": "8.18.0",
+ "@typescript-eslint/visitor-keys": "8.18.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz",
+ "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.18.0",
+ "@typescript-eslint/visitor-keys": "8.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz",
+ "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.18.0",
+ "@typescript-eslint/utils": "8.18.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz",
+ "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz",
+ "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.18.0",
+ "@typescript-eslint/visitor-keys": "8.18.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz",
+ "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.18.0",
+ "@typescript-eslint/types": "8.18.0",
+ "@typescript-eslint/typescript-estree": "8.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz",
+ "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.18.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz",
+ "integrity": "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@swc/core": "^1.7.26"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/dompurify": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
+ "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
+ "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.0",
+ "@esbuild/android-arm": "0.24.0",
+ "@esbuild/android-arm64": "0.24.0",
+ "@esbuild/android-x64": "0.24.0",
+ "@esbuild/darwin-arm64": "0.24.0",
+ "@esbuild/darwin-x64": "0.24.0",
+ "@esbuild/freebsd-arm64": "0.24.0",
+ "@esbuild/freebsd-x64": "0.24.0",
+ "@esbuild/linux-arm": "0.24.0",
+ "@esbuild/linux-arm64": "0.24.0",
+ "@esbuild/linux-ia32": "0.24.0",
+ "@esbuild/linux-loong64": "0.24.0",
+ "@esbuild/linux-mips64el": "0.24.0",
+ "@esbuild/linux-ppc64": "0.24.0",
+ "@esbuild/linux-riscv64": "0.24.0",
+ "@esbuild/linux-s390x": "0.24.0",
+ "@esbuild/linux-x64": "0.24.0",
+ "@esbuild/netbsd-x64": "0.24.0",
+ "@esbuild/openbsd-arm64": "0.24.0",
+ "@esbuild/openbsd-x64": "0.24.0",
+ "@esbuild/sunos-x64": "0.24.0",
+ "@esbuild/win32-arm64": "0.24.0",
+ "@esbuild/win32-ia32": "0.24.0",
+ "@esbuild/win32-x64": "0.24.0"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.16.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz",
+ "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.19.0",
+ "@eslint/core": "^0.9.0",
+ "@eslint/eslintrc": "^3.2.0",
+ "@eslint/js": "9.16.0",
+ "@eslint/plugin-kit": "^0.2.3",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.1",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.5",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
+ "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz",
+ "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+ "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
+ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
+ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.25.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.2.tgz",
+ "integrity": "sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookie": "^0.6.0",
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0",
+ "turbo-stream": "2.4.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.2.tgz",
+ "integrity": "sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.0.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
+ "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.28.1",
+ "@rollup/rollup-android-arm64": "4.28.1",
+ "@rollup/rollup-darwin-arm64": "4.28.1",
+ "@rollup/rollup-darwin-x64": "4.28.1",
+ "@rollup/rollup-freebsd-arm64": "4.28.1",
+ "@rollup/rollup-freebsd-x64": "4.28.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.28.1",
+ "@rollup/rollup-linux-arm64-musl": "4.28.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.28.1",
+ "@rollup/rollup-linux-x64-gnu": "4.28.1",
+ "@rollup/rollup-linux-x64-musl": "4.28.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.28.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.28.1",
+ "@rollup/rollup-win32-x64-msvc": "4.28.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/turbo-stream": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
+ "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
+ "license": "ISC"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz",
+ "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.24.0",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
index 73167b9f..bdafb8f7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,35 +1,32 @@
-{
- "name": "tubearchivist-frontend",
- "version": "0.1.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "build:deploy": "vite build",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview"
- },
- "dependencies": {
- "dompurify": "^3.1.6",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-helmet": "^6.1.0",
- "react-router-dom": "^6.25.1"
- },
- "devDependencies": {
- "@types/dompurify": "^3.0.5",
- "@types/react": "^18.3.3",
- "@types/react-dom": "^18.3.0",
- "@types/react-helmet": "^6.1.11",
- "@typescript-eslint/eslint-plugin": "^7.16.1",
- "@typescript-eslint/parser": "^7.16.1",
- "@vitejs/plugin-react-swc": "^3.7.0",
- "eslint": "^8.57.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-plugin-react-hooks": "^4.6.2",
- "eslint-plugin-react-refresh": "^0.4.8",
- "prettier": "3.3.3",
- "typescript": "^5.5.3",
- "vite": "^5.3.4"
- }
-}
+{
+ "name": "tubearchivist-frontend",
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "build:deploy": "vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "dompurify": "^3.2.3",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.0.2"
+ },
+ "devDependencies": {
+ "@types/react": "^19.0.1",
+ "@types/react-dom": "^19.0.2",
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
+ "@typescript-eslint/parser": "^8.18.0",
+ "@vitejs/plugin-react-swc": "^3.7.2",
+ "eslint": "^9.16.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "prettier": "3.4.2",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.3"
+ }
+}
diff --git a/frontend/src/api/actions/createAppriseNotificationUrl.ts b/frontend/src/api/actions/createAppriseNotificationUrl.ts
new file mode 100644
index 00000000..a3054517
--- /dev/null
+++ b/frontend/src/api/actions/createAppriseNotificationUrl.ts
@@ -0,0 +1,36 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+export type AppriseTaskNameType =
+ | 'update_subscribed'
+ | 'extract_download'
+ | 'download_pending'
+ | 'check_reindex';
+
+const createAppriseNotificationUrl = async (taskName: AppriseTaskNameType, url: string) => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ const response = await fetch(`${apiUrl}/api/task/notification/`, {
+ method: 'POST',
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+ body: JSON.stringify({ task_name: taskName, url }),
+ });
+
+ const appriseNotificationUrl = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('createAppriseNotificationUrl', appriseNotificationUrl);
+ }
+
+ return appriseNotificationUrl;
+};
+
+export default createAppriseNotificationUrl;
diff --git a/frontend/src/api/actions/createTaskSchedule.ts b/frontend/src/api/actions/createTaskSchedule.ts
new file mode 100644
index 00000000..d27ee4d7
--- /dev/null
+++ b/frontend/src/api/actions/createTaskSchedule.ts
@@ -0,0 +1,53 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+export type TaskScheduleNameType =
+ | 'update_subscribed'
+ | 'download_pending'
+ | 'extract_download'
+ | 'check_reindex'
+ | 'manual_import'
+ | 'run_backup'
+ | 'restore_backup'
+ | 'rescan_filesystem'
+ | 'thumbnail_check'
+ | 'resync_thumbs'
+ | 'index_playlists'
+ | 'subscribe_to'
+ | 'version_check';
+
+type ScheduleConfigType = {
+ schedule?: string;
+ config?: {
+ days?: number;
+ rotate?: number;
+ };
+};
+
+const createTaskSchedule = async (taskName: TaskScheduleNameType, schedule: ScheduleConfigType) => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ const response = await fetch(`${apiUrl}/api/task/schedule/${taskName}/`, {
+ method: 'POST',
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+ body: JSON.stringify(schedule),
+ });
+
+ const scheduledTask = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('createTaskSchedule', scheduledTask);
+ }
+
+ return scheduledTask;
+};
+
+export default createTaskSchedule;
diff --git a/frontend/src/api/actions/deleteAppriseNotificationUrl.ts b/frontend/src/api/actions/deleteAppriseNotificationUrl.ts
new file mode 100644
index 00000000..0738dc5b
--- /dev/null
+++ b/frontend/src/api/actions/deleteAppriseNotificationUrl.ts
@@ -0,0 +1,36 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+type AppriseTaskNameType =
+ | 'update_subscribed'
+ | 'extract_download'
+ | 'download_pending'
+ | 'check_reindex';
+
+const deleteAppriseNotificationUrl = async (taskName: AppriseTaskNameType) => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ const response = await fetch(`${apiUrl}/api/task/notification/`, {
+ method: 'DELETE',
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+ body: JSON.stringify({ task_name: taskName }),
+ });
+
+ const appriseNotification = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('deleteAppriseNotificationUrl', appriseNotification);
+ }
+
+ return appriseNotification;
+};
+
+export default deleteAppriseNotificationUrl;
diff --git a/frontend/src/api/actions/deleteTaskSchedule.ts b/frontend/src/api/actions/deleteTaskSchedule.ts
new file mode 100644
index 00000000..26f17650
--- /dev/null
+++ b/frontend/src/api/actions/deleteTaskSchedule.ts
@@ -0,0 +1,30 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+import { TaskScheduleNameType } from './createTaskSchedule';
+
+const deleteTaskSchedule = async (taskName: TaskScheduleNameType) => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ const response = await fetch(`${apiUrl}/api/task/schedule/${taskName}/`, {
+ method: 'DELETE',
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+ });
+
+ const scheduledTask = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('deleteTaskSchedule', scheduledTask);
+ }
+
+ return scheduledTask;
+};
+
+export default deleteTaskSchedule;
diff --git a/frontend/src/api/actions/updateChannelSettings.ts b/frontend/src/api/actions/updateChannelSettings.ts
new file mode 100644
index 00000000..d7b059ea
--- /dev/null
+++ b/frontend/src/api/actions/updateChannelSettings.ts
@@ -0,0 +1,39 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+
+export type ChannelAboutConfigType = {
+ index_playlists?: boolean;
+ download_format?: boolean | string;
+ autodelete_days?: boolean | number;
+ integrate_sponsorblock?: boolean | null;
+ subscriptions_channel_size?: number;
+ subscriptions_live_channel_size?: number;
+ subscriptions_shorts_channel_size?: number;
+};
+
+const updateChannelSettings = async (channelId: string, config: ChannelAboutConfigType) => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ const response = await fetch(`${apiUrl}/api/channel/${channelId}/`, {
+ method: 'POST',
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+
+ body: JSON.stringify({
+ channel_overwrites: config,
+ }),
+ });
+
+ const channelSubscription = await response.json();
+ console.log('updateChannelSettings', channelSubscription);
+
+ return channelSubscription;
+};
+
+export default updateChannelSettings;
diff --git a/frontend/src/api/actions/updateCookie.ts b/frontend/src/api/actions/updateCookie.ts
index d02ba71e..f8aa2f3c 100644
--- a/frontend/src/api/actions/updateCookie.ts
+++ b/frontend/src/api/actions/updateCookie.ts
@@ -1,32 +1,33 @@
-import defaultHeaders from '../../configuration/defaultHeaders';
-import getApiUrl from '../../configuration/getApiUrl';
-import getFetchCredentials from '../../configuration/getFetchCredentials';
-import getCookie from '../../functions/getCookie';
-
-export type ValidatedCookieType = {
- cookie_enabled: boolean;
- status: boolean;
- validated: number;
- validated_str: string;
-};
-
-const updateCookie = async (): Promise => {
- const apiUrl = getApiUrl();
- const csrfCookie = getCookie('csrftoken');
-
- const response = await fetch(`${apiUrl}/api/appsettings/cookie/`, {
- method: 'POST',
- headers: {
- ...defaultHeaders,
- 'X-CSRFToken': csrfCookie || '',
- },
- credentials: getFetchCredentials(),
- });
-
- const validatedCookie = await response.json();
- console.log('updateCookie', validatedCookie);
-
- return validatedCookie;
-};
-
-export default updateCookie;
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+
+export type ValidatedCookieType = {
+ cookie_enabled: boolean;
+ status: boolean;
+ validated: number;
+ validated_str: string;
+ cookie_validated?: boolean;
+};
+
+const updateCookie = async (): Promise => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ const response = await fetch(`${apiUrl}/api/appsettings/cookie/`, {
+ method: 'POST',
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+ });
+
+ const validatedCookie = await response.json();
+ console.log('updateCookie', validatedCookie);
+
+ return validatedCookie;
+};
+
+export default updateCookie;
diff --git a/frontend/src/api/loader/loadApiToken.ts b/frontend/src/api/loader/loadApiToken.ts
index 965433ff..17646e9e 100644
--- a/frontend/src/api/loader/loadApiToken.ts
+++ b/frontend/src/api/loader/loadApiToken.ts
@@ -1,32 +1,36 @@
-import defaultHeaders from '../../configuration/defaultHeaders';
-import getApiUrl from '../../configuration/getApiUrl';
-import getFetchCredentials from '../../configuration/getFetchCredentials';
-import getCookie from '../../functions/getCookie';
-import isDevEnvironment from '../../functions/isDevEnvironment';
-
-type ApiTokenResponse = {
- token: string;
-};
-
-const loadApiToken = async (): Promise => {
- const apiUrl = getApiUrl();
- const csrfCookie = getCookie('csrftoken');
-
- const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
- headers: {
- ...defaultHeaders,
- 'X-CSRFToken': csrfCookie || '',
- },
- credentials: getFetchCredentials(),
- });
-
- const apiToken = await response.json();
-
- if (isDevEnvironment()) {
- console.log('loadApiToken', apiToken);
- }
-
- return apiToken;
-};
-
-export default loadApiToken;
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import getCookie from '../../functions/getCookie';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+type ApiTokenResponse = {
+ token: string;
+};
+
+const loadApiToken = async (): Promise => {
+ const apiUrl = getApiUrl();
+ const csrfCookie = getCookie('csrftoken');
+
+ try {
+ const response = await fetch(`${apiUrl}/api/appsettings/token/`, {
+ headers: {
+ ...defaultHeaders,
+ 'X-CSRFToken': csrfCookie || '',
+ },
+ credentials: getFetchCredentials(),
+ });
+
+ const apiToken = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('loadApiToken', apiToken);
+ }
+
+ return apiToken;
+ } catch (e) {
+ return { token: '' };
+ }
+};
+
+export default loadApiToken;
diff --git a/frontend/src/api/loader/loadAppriseNotification.ts b/frontend/src/api/loader/loadAppriseNotification.ts
new file mode 100644
index 00000000..3e1a1449
--- /dev/null
+++ b/frontend/src/api/loader/loadAppriseNotification.ts
@@ -0,0 +1,42 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+export type AppriseNotificationType = {
+ check_reindex?: {
+ urls: string[];
+ title: string;
+ };
+ download_pending?: {
+ urls: string[];
+ title: string;
+ };
+ extract_download?: {
+ urls: string[];
+ title: string;
+ };
+ update_subscribed?: {
+ urls: string[];
+ title: string;
+ };
+};
+
+const loadAppriseNotification = async (): Promise => {
+ const apiUrl = getApiUrl();
+
+ const response = await fetch(`${apiUrl}/api/task/notification/`, {
+ headers: defaultHeaders,
+ credentials: getFetchCredentials(),
+ });
+
+ const notification = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('loadAppriseNotification', notification);
+ }
+
+ return notification;
+};
+
+export default loadAppriseNotification;
diff --git a/frontend/src/api/loader/loadAppsettingsConfig.ts b/frontend/src/api/loader/loadAppsettingsConfig.ts
index 90b50b76..293007b5 100644
--- a/frontend/src/api/loader/loadAppsettingsConfig.ts
+++ b/frontend/src/api/loader/loadAppsettingsConfig.ts
@@ -1,54 +1,54 @@
-import defaultHeaders from '../../configuration/defaultHeaders';
-import getApiUrl from '../../configuration/getApiUrl';
-import getFetchCredentials from '../../configuration/getFetchCredentials';
-import isDevEnvironment from '../../functions/isDevEnvironment';
-
-export type AppSettingsConfigType = {
- subscriptions: {
- channel_size: number;
- live_channel_size: number;
- shorts_channel_size: number;
- auto_start: boolean;
- };
- downloads: {
- limit_speed: boolean | number;
- sleep_interval: number;
- autodelete_days: boolean | number;
- format: boolean | string;
- format_sort: boolean | string;
- add_metadata: boolean;
- add_thumbnail: boolean;
- subtitle: boolean | string;
- subtitle_source: boolean | string;
- subtitle_index: boolean;
- comment_max: boolean | number;
- comment_sort: string;
- cookie_import: boolean;
- throttledratelimit: boolean | number;
- extractor_lang: boolean | string;
- integrate_ryd: boolean;
- integrate_sponsorblock: boolean;
- };
- application: {
- enable_snapshot: boolean;
- };
-};
-
-const loadAppsettingsConfig = async (): Promise => {
- const apiUrl = getApiUrl();
-
- const response = await fetch(`${apiUrl}/api/appsettings/config/`, {
- headers: defaultHeaders,
- credentials: getFetchCredentials(),
- });
-
- const appSettingsConfig = await response.json();
-
- if (isDevEnvironment()) {
- console.log('loadApplicationConfig', appSettingsConfig);
- }
-
- return appSettingsConfig;
-};
-
-export default loadAppsettingsConfig;
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+export type AppSettingsConfigType = {
+ subscriptions: {
+ channel_size: number;
+ live_channel_size: number;
+ shorts_channel_size: number;
+ auto_start: boolean;
+ };
+ downloads: {
+ limit_speed: false | number;
+ sleep_interval: number;
+ autodelete_days: number;
+ format: number | string;
+ format_sort: boolean | string;
+ add_metadata: boolean;
+ add_thumbnail: boolean;
+ subtitle: boolean | string;
+ subtitle_source: boolean | string;
+ subtitle_index: boolean;
+ comment_max: string | number;
+ comment_sort: string;
+ cookie_import: boolean;
+ throttledratelimit: false | number;
+ extractor_lang: boolean | string;
+ integrate_ryd: boolean;
+ integrate_sponsorblock: boolean;
+ };
+ application: {
+ enable_snapshot: boolean;
+ };
+};
+
+const loadAppsettingsConfig = async (): Promise => {
+ const apiUrl = getApiUrl();
+
+ const response = await fetch(`${apiUrl}/api/appsettings/config/`, {
+ headers: defaultHeaders,
+ credentials: getFetchCredentials(),
+ });
+
+ const appSettingsConfig = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('loadApplicationConfig', appSettingsConfig);
+ }
+
+ return appSettingsConfig;
+};
+
+export default loadAppsettingsConfig;
diff --git a/frontend/src/api/loader/loadChannelAggs.ts b/frontend/src/api/loader/loadChannelAggs.ts
new file mode 100644
index 00000000..e3d175d9
--- /dev/null
+++ b/frontend/src/api/loader/loadChannelAggs.ts
@@ -0,0 +1,36 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+export type ChannelAggsType = {
+ total_items: {
+ value: number;
+ };
+ total_size: {
+ value: number;
+ };
+ total_duration: {
+ value: number;
+ value_str: string;
+ };
+};
+
+const loadChannelAggs = async (channelId: string): Promise => {
+ const apiUrl = getApiUrl();
+
+ const response = await fetch(`${apiUrl}/api/channel/${channelId}/aggs/`, {
+ headers: defaultHeaders,
+ credentials: getFetchCredentials(),
+ });
+
+ const channel = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('loadChannelAggs', channel);
+ }
+
+ return channel;
+};
+
+export default loadChannelAggs;
diff --git a/frontend/src/api/loader/loadSchedule.ts b/frontend/src/api/loader/loadSchedule.ts
new file mode 100644
index 00000000..e67b73e3
--- /dev/null
+++ b/frontend/src/api/loader/loadSchedule.ts
@@ -0,0 +1,36 @@
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+
+type ScheduleType = {
+ name: string;
+ schedule: string;
+ schedule_human: string;
+ last_run_at: string;
+ config: {
+ days?: number;
+ rotate?: number;
+ };
+};
+
+export type ScheduleResponseType = ScheduleType[];
+
+const loadSchedule = async (): Promise => {
+ const apiUrl = getApiUrl();
+
+ const response = await fetch(`${apiUrl}/api/task/schedule/`, {
+ headers: defaultHeaders,
+ credentials: getFetchCredentials(),
+ });
+
+ const schedule = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('loadSchedule', schedule);
+ }
+
+ return schedule;
+};
+
+export default loadSchedule;
diff --git a/frontend/src/api/loader/loadVideoListByPage.ts b/frontend/src/api/loader/loadVideoListByPage.ts
index edf6cd4b..465bad59 100644
--- a/frontend/src/api/loader/loadVideoListByPage.ts
+++ b/frontend/src/api/loader/loadVideoListByPage.ts
@@ -1,74 +1,74 @@
-import defaultHeaders from '../../configuration/defaultHeaders';
-import getApiUrl from '../../configuration/getApiUrl';
-import getFetchCredentials from '../../configuration/getFetchCredentials';
-import isDevEnvironment from '../../functions/isDevEnvironment';
-import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
-import { PaginationType } from '../../components/Pagination';
-
-export type VideoListByFilterResponseType = {
- data?: VideoType[];
- config?: ConfigType;
- paginate?: PaginationType;
-};
-
-type WatchTypes = 'watched' | 'unwatched' | 'continue';
-type VideoTypes = 'videos' | 'streams' | 'shorts';
-
-type FilterType = {
- page?: number;
- playlist?: string;
- channel?: string;
- watch?: WatchTypes;
- sort?: SortByType;
- order?: SortOrderType;
- type?: VideoTypes;
-};
-
-const loadVideoListByFilter = async (
- filter: FilterType,
-): Promise => {
- const apiUrl = getApiUrl();
-
- const searchParams = new URLSearchParams();
-
- if (filter.page) {
- searchParams.append('page', filter.page.toString());
- }
-
- if (filter.playlist) {
- searchParams.append('playlist', filter.playlist);
- } else if (filter.channel) {
- searchParams.append('channel', filter.channel);
- }
-
- if (filter.watch) {
- searchParams.append('watch', filter.watch);
- }
-
- if (filter.sort) {
- searchParams.append('sort', filter.sort);
- }
-
- if (filter.order) {
- searchParams.append('order', filter.order);
- }
-
- if (filter.type) {
- searchParams.append('type', filter.type);
- }
-
- const response = await fetch(`${apiUrl}/api/video/?${searchParams.toString()}`, {
- headers: defaultHeaders,
- credentials: getFetchCredentials(),
- });
-
- const videos = await response.json();
-
- if (isDevEnvironment()) {
- console.log('loadVideoListByFilter', filter, videos);
- }
-
- return videos;
-};
-
-export default loadVideoListByFilter;
+import defaultHeaders from '../../configuration/defaultHeaders';
+import getApiUrl from '../../configuration/getApiUrl';
+import getFetchCredentials from '../../configuration/getFetchCredentials';
+import isDevEnvironment from '../../functions/isDevEnvironment';
+import { ConfigType, SortByType, SortOrderType, VideoType } from '../../pages/Home';
+import { PaginationType } from '../../components/Pagination';
+
+export type VideoListByFilterResponseType = {
+ data?: VideoType[];
+ config?: ConfigType;
+ paginate?: PaginationType;
+};
+
+type WatchTypes = 'watched' | 'unwatched' | 'continue';
+export type VideoTypes = 'videos' | 'streams' | 'shorts';
+
+type FilterType = {
+ page?: number;
+ playlist?: string;
+ channel?: string;
+ watch?: WatchTypes;
+ sort?: SortByType;
+ order?: SortOrderType;
+ type?: VideoTypes;
+};
+
+const loadVideoListByFilter = async (
+ filter: FilterType,
+): Promise => {
+ const apiUrl = getApiUrl();
+
+ const searchParams = new URLSearchParams();
+
+ if (filter.page) {
+ searchParams.append('page', filter.page.toString());
+ }
+
+ if (filter.playlist) {
+ searchParams.append('playlist', filter.playlist);
+ } else if (filter.channel) {
+ searchParams.append('channel', filter.channel);
+ }
+
+ if (filter.watch) {
+ searchParams.append('watch', filter.watch);
+ }
+
+ if (filter.sort) {
+ searchParams.append('sort', filter.sort);
+ }
+
+ if (filter.order) {
+ searchParams.append('order', filter.order);
+ }
+
+ if (filter.type) {
+ searchParams.append('type', filter.type);
+ }
+
+ const response = await fetch(`${apiUrl}/api/video/?${searchParams.toString()}`, {
+ headers: defaultHeaders,
+ credentials: getFetchCredentials(),
+ });
+
+ const videos = await response.json();
+
+ if (isDevEnvironment()) {
+ console.log('loadVideoListByFilter', filter, videos);
+ }
+
+ return videos;
+};
+
+export default loadVideoListByFilter;
diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx
index 3ba74841..2d97351f 100644
--- a/frontend/src/components/Button.tsx
+++ b/frontend/src/components/Button.tsx
@@ -1,40 +1,42 @@
-export interface ButtonProps {
- id?: string;
- name?: string;
- className?: string;
- type?: 'submit' | 'reset' | 'button' | undefined;
- label?: string | JSX.Element | JSX.Element[];
- children?: string | JSX.Element | JSX.Element[];
- value?: string;
- title?: string;
- onClick?: () => void;
-}
-
-const Button = ({
- id,
- name,
- className,
- type,
- label,
- children,
- value,
- title,
- onClick,
-}: ButtonProps) => {
- return (
-
- {label}
- {children}
-
- );
-};
-
-export default Button;
+import { ReactNode } from 'react';
+
+export interface ButtonProps {
+ id?: string;
+ name?: string;
+ className?: string;
+ type?: 'submit' | 'reset' | 'button' | undefined;
+ label?: string | ReactNode | ReactNode[];
+ children?: string | ReactNode | ReactNode[];
+ value?: string;
+ title?: string;
+ onClick?: () => void;
+}
+
+const Button = ({
+ id,
+ name,
+ className,
+ type,
+ label,
+ children,
+ value,
+ title,
+ onClick,
+}: ButtonProps) => {
+ return (
+
+ {label}
+ {children}
+
+ );
+};
+
+export default Button;
diff --git a/frontend/src/components/ChannelBanner.tsx b/frontend/src/components/ChannelBanner.tsx
index 49a09442..a0580320 100644
--- a/frontend/src/components/ChannelBanner.tsx
+++ b/frontend/src/components/ChannelBanner.tsx
@@ -1,21 +1,22 @@
-import getApiUrl from '../configuration/getApiUrl';
-import defaultChannelImage from '/img/default-channel-banner.jpg';
-
-type ChannelIconProps = {
- channel_id: string;
-};
-
-const ChannelBanner = ({ channel_id }: ChannelIconProps) => {
- return (
- {
- currentTarget.onerror = null; // prevents looping
- currentTarget.src = defaultChannelImage;
- }}
- />
- );
-};
-
-export default ChannelBanner;
+import getApiUrl from '../configuration/getApiUrl';
+import defaultChannelImage from '/img/default-channel-banner.jpg';
+
+type ChannelIconProps = {
+ channelId: string;
+ channelBannerUrl: string | undefined;
+};
+
+const ChannelBanner = ({ channelId, channelBannerUrl }: ChannelIconProps) => {
+ return (
+ {
+ currentTarget.onerror = null; // prevents looping
+ currentTarget.src = defaultChannelImage;
+ }}
+ />
+ );
+};
+
+export default ChannelBanner;
diff --git a/frontend/src/components/ChannelIcon.tsx b/frontend/src/components/ChannelIcon.tsx
index 383cc5bb..0968c00e 100644
--- a/frontend/src/components/ChannelIcon.tsx
+++ b/frontend/src/components/ChannelIcon.tsx
@@ -1,21 +1,22 @@
-import getApiUrl from '../configuration/getApiUrl';
-import defaultChannelIcon from '/img/default-channel-icon.jpg';
-
-type ChannelIconProps = {
- channel_id: string;
-};
-
-const ChannelIcon = ({ channel_id }: ChannelIconProps) => {
- return (
- {
- currentTarget.onerror = null; // prevents looping
- currentTarget.src = defaultChannelIcon;
- }}
- />
- );
-};
-
-export default ChannelIcon;
+import getApiUrl from '../configuration/getApiUrl';
+import defaultChannelIcon from '/img/default-channel-icon.jpg';
+
+type ChannelIconProps = {
+ channelId: string;
+ channelThumbUrl: string | undefined;
+};
+
+const ChannelIcon = ({ channelId, channelThumbUrl }: ChannelIconProps) => {
+ return (
+ {
+ currentTarget.onerror = null; // prevents looping
+ currentTarget.src = defaultChannelIcon;
+ }}
+ />
+ );
+};
+
+export default ChannelIcon;
diff --git a/frontend/src/components/ChannelList.tsx b/frontend/src/components/ChannelList.tsx
index b1a2ad07..a633809a 100644
--- a/frontend/src/components/ChannelList.tsx
+++ b/frontend/src/components/ChannelList.tsx
@@ -1,83 +1,93 @@
-import { Link } from 'react-router-dom';
-import { ChannelType } from '../pages/Channels';
-import { ViewLayoutType } from '../pages/Home';
-import Routes from '../configuration/routes/RouteList';
-import updateChannelSubscription from '../api/actions/updateChannelSubscription';
-import formatDate from '../functions/formatDates';
-import FormattedNumber from './FormattedNumber';
-import Button from './Button';
-import ChannelIcon from './ChannelIcon';
-import ChannelBanner from './ChannelBanner';
-
-type ChannelListProps = {
- channelList: ChannelType[] | undefined;
- viewLayout: ViewLayoutType;
- refreshChannelList: (refresh: boolean) => void;
-};
-
-const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelListProps) => {
- if (!channelList || channelList.length === 0) {
- return No channels found.
;
- }
-
- return (
- <>
- {channelList.map(channel => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {channel.channel_name}
-
-
-
-
-
-
-
Last refreshed: {formatDate(channel.channel_last_refresh)}
- {channel.channel_subscribed && (
-
{
- await updateChannelSubscription(channel.channel_id, false);
- refreshChannelList(true);
- }}
- />
- )}
- {!channel.channel_subscribed && (
- {
- await updateChannelSubscription(channel.channel_id, true);
- refreshChannelList(true);
- }}
- />
- )}
-
-
-
-
- );
- })}
- >
- );
-};
-
-export default ChannelList;
+import { Link } from 'react-router-dom';
+import { ChannelType } from '../pages/Channels';
+import { ViewLayoutType } from '../pages/Home';
+import Routes from '../configuration/routes/RouteList';
+import updateChannelSubscription from '../api/actions/updateChannelSubscription';
+import formatDate from '../functions/formatDates';
+import FormattedNumber from './FormattedNumber';
+import Button from './Button';
+import ChannelIcon from './ChannelIcon';
+import ChannelBanner from './ChannelBanner';
+
+type ChannelListProps = {
+ channelList: ChannelType[] | undefined;
+ viewLayout: ViewLayoutType;
+ refreshChannelList: (refresh: boolean) => void;
+};
+
+const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelListProps) => {
+ if (!channelList || channelList.length === 0) {
+ return No channels found.
;
+ }
+
+ return (
+ <>
+ {channelList.map(channel => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {channel.channel_name}
+
+
+
+
+
+
+
Last refreshed: {formatDate(channel.channel_last_refresh)}
+ {channel.channel_subscribed && (
+
{
+ await updateChannelSubscription(channel.channel_id, false);
+ refreshChannelList(true);
+ }}
+ />
+ )}
+
+ {!channel.channel_subscribed && (
+ {
+ await updateChannelSubscription(channel.channel_id, true);
+
+ setTimeout(() => {
+ refreshChannelList(true);
+ }, 500);
+ }}
+ />
+ )}
+
+
+
+
+ );
+ })}
+ >
+ );
+};
+
+export default ChannelList;
diff --git a/frontend/src/components/ChannelOverview.tsx b/frontend/src/components/ChannelOverview.tsx
index c4c181dd..9c5283fd 100644
--- a/frontend/src/components/ChannelOverview.tsx
+++ b/frontend/src/components/ChannelOverview.tsx
@@ -1,76 +1,78 @@
-import { Link } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import updateChannelSubscription from '../api/actions/updateChannelSubscription';
-import FormattedNumber from './FormattedNumber';
-import Button from './Button';
-import ChannelIcon from './ChannelIcon';
-
-type ChannelOverviewProps = {
- channelId: string;
- channelname: string;
- channelSubs: number;
- channelSubscribed: boolean;
- showSubscribeButton?: boolean;
- isUserAdmin?: boolean;
- setRefresh: (status: boolean) => void;
-};
-
-const ChannelOverview = ({
- channelId,
- channelSubs,
- channelSubscribed,
- channelname,
- showSubscribeButton = false,
- isUserAdmin,
- setRefresh,
-}: ChannelOverviewProps) => {
- return (
- <>
-
-
-
-
-
-
-
-
- {channelname}
-
-
-
-
- {showSubscribeButton && (
- <>
- {channelSubscribed && isUserAdmin && (
- {
- await updateChannelSubscription(channelId, false);
- setRefresh(true);
- }}
- />
- )}
-
- {!channelSubscribed && (
- {
- await updateChannelSubscription(channelId, true);
- setRefresh(true);
- }}
- />
- )}
- >
- )}
-
-
- >
- );
-};
-
-export default ChannelOverview;
+import { Link } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import updateChannelSubscription from '../api/actions/updateChannelSubscription';
+import FormattedNumber from './FormattedNumber';
+import Button from './Button';
+import ChannelIcon from './ChannelIcon';
+
+type ChannelOverviewProps = {
+ channelId: string;
+ channelname: string;
+ channelSubs: number;
+ channelSubscribed: boolean;
+ channelThumbUrl: string;
+ showSubscribeButton?: boolean;
+ isUserAdmin?: boolean;
+ setRefresh: (status: boolean) => void;
+};
+
+const ChannelOverview = ({
+ channelId,
+ channelSubs,
+ channelSubscribed,
+ channelname,
+ channelThumbUrl,
+ showSubscribeButton = false,
+ isUserAdmin,
+ setRefresh,
+}: ChannelOverviewProps) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {channelname}
+
+
+
+
+ {showSubscribeButton && (
+ <>
+ {channelSubscribed && isUserAdmin && (
+ {
+ await updateChannelSubscription(channelId, false);
+ setRefresh(true);
+ }}
+ />
+ )}
+
+ {!channelSubscribed && (
+ {
+ await updateChannelSubscription(channelId, true);
+ setRefresh(true);
+ }}
+ />
+ )}
+ >
+ )}
+
+
+ >
+ );
+};
+
+export default ChannelOverview;
diff --git a/frontend/src/components/EmbeddableVideoPlayer.tsx b/frontend/src/components/EmbeddableVideoPlayer.tsx
index 24fecd36..cd6a8341 100644
--- a/frontend/src/components/EmbeddableVideoPlayer.tsx
+++ b/frontend/src/components/EmbeddableVideoPlayer.tsx
@@ -115,7 +115,8 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
id: videoId,
is_watched: status,
});
-
+ }}
+ onDone={() => {
setRefresh(true);
}}
/>
diff --git a/frontend/src/components/Filterbar.tsx b/frontend/src/components/Filterbar.tsx
index fc40537e..3bb1aac3 100644
--- a/frontend/src/components/Filterbar.tsx
+++ b/frontend/src/components/Filterbar.tsx
@@ -48,24 +48,16 @@ const Filterbar = ({
}: FilterbarProps) => {
useEffect(() => {
(async () => {
- if (
- userMeConfig.hide_watched !== hideWatched ||
- userMeConfig[viewStyleName.toString() as keyof typeof userMeConfig] !== view ||
- userMeConfig.grid_items !== gridItems ||
- userMeConfig.sort_by !== sortBy ||
- userMeConfig.sort_order !== sortOrder
- ) {
- const userConfig: UserConfigType = {
- hide_watched: hideWatched,
- [viewStyleName.toString()]: view,
- grid_items: gridItems,
- sort_by: sortBy,
- sort_order: sortOrder,
- };
+ const userConfig: UserConfigType = {
+ hide_watched: hideWatched,
+ [viewStyleName.toString()]: view,
+ grid_items: gridItems,
+ sort_by: sortBy,
+ sort_order: sortOrder,
+ };
- await updateUserConfig(userConfig);
- setRefresh?.(true);
- }
+ await updateUserConfig(userConfig);
+ setRefresh?.(true);
})();
}, [hideWatched, view, gridItems, sortBy, sortOrder, viewStyleName, setRefresh, userMeConfig]);
diff --git a/frontend/src/components/GoogleCast.tsx b/frontend/src/components/GoogleCast.tsx
index 55a0680b..a22c1104 100644
--- a/frontend/src/components/GoogleCast.tsx
+++ b/frontend/src/components/GoogleCast.tsx
@@ -1,230 +1,228 @@
-import { useCallback, useEffect, useState } from 'react';
-import { Helmet } from 'react-helmet';
-import { VideoType } from '../pages/Home';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import updateVideoProgressById from '../api/actions/updateVideoProgressById';
-import watchedThreshold from '../functions/watchedThreshold';
-import { VideoProgressType } from './VideoPlayer';
-
-const getURL = () => {
- return window.location.origin;
-};
-
-function shiftCurrentTime(contentCurrentTime: number | undefined) {
- console.log(contentCurrentTime);
- if (contentCurrentTime === undefined) {
- return 0;
- }
-
- // Shift media back 3 seconds to prevent missing some of the content
- if (contentCurrentTime > 5) {
- return contentCurrentTime - 3;
- } else {
- return 0;
- }
-}
-
-async function castVideoProgress(
- player: {
- mediaInfo: { contentId: string | string[] };
- currentTime: number;
- duration: number;
- },
- video: VideoType | undefined,
-) {
- if (!video) {
- console.log('castVideoProgress: Video to cast not found...');
- return;
- }
- const videoId = video.youtube_id;
-
- if (player.mediaInfo.contentId.includes(videoId)) {
- const currentTime = player.currentTime;
- const duration = player.duration;
-
- if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
- // Check progress every 10 seconds or else progress is checked a few times a second
- await updateVideoProgressById({
- youtubeId: videoId,
- currentProgress: currentTime,
- });
-
- if (!video.player.watched) {
- // Check if video is already marked as watched
- if (watchedThreshold(currentTime, duration)) {
- await updateWatchedState({
- id: videoId,
- is_watched: true,
- });
- }
- }
- }
- }
-}
-
-async function castVideoPaused(
- player: {
- currentTime: number;
- duration: number;
- mediaInfo: { contentId: string | string[] } | null;
- },
- video: VideoType | undefined,
-) {
- if (!video) {
- console.log('castVideoPaused: Video to cast not found...');
- return;
- }
-
- const videoId = video?.youtube_id;
-
- const currentTime = player.currentTime;
- const duration = player.duration;
-
- if (player.mediaInfo != null) {
- if (player.mediaInfo.contentId.includes(videoId)) {
- if (currentTime !== 0 && duration !== 0) {
- await updateVideoProgressById({
- youtubeId: videoId,
- currentProgress: currentTime,
- });
- }
- }
- }
-}
-
-type GoogleCastProps = {
- video?: VideoType;
- videoProgress?: VideoProgressType;
- setRefresh?: () => void;
-};
-
-const GoogleCast = ({ video, videoProgress, setRefresh }: GoogleCastProps) => {
- const [isConnected, setIsConnected] = useState(false);
-
- const setup = useCallback(() => {
- const cast = globalThis.cast;
- const chrome = globalThis.chrome;
-
- 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.
- autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
- });
-
- const player = new cast.framework.RemotePlayer();
-
- const playerController = new cast.framework.RemotePlayerController(player);
-
- // Add event listerner to check if a connection to a cast device is initiated
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
- function () {
- setIsConnected(player.isConnected);
- },
- );
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
- function () {
- castVideoProgress(player, video);
- },
- );
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
- function () {
- castVideoPaused(player, video);
- setRefresh?.();
- },
- );
- }, [setRefresh, video]);
-
- const startPlayback = useCallback(() => {
- const chrome = globalThis.chrome;
- const cast = globalThis.cast;
- const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
-
- const mediaUrl = video?.media_url;
- const vidThumbUrl = video?.vid_thumb_url;
- const contentTitle = video?.title;
- const contentId = `${getURL()}${mediaUrl}`;
- const contentImage = `${getURL()}${vidThumbUrl}`;
- const contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
-
- const contentSubtitles = [];
- const videoSubtitles = video?.subtitles; // Array of subtitles
- if (typeof videoSubtitles !== 'undefined') {
- for (let i = 0; i < videoSubtitles.length; i++) {
- const subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
-
- subtitle.trackContentId = videoSubtitles[i].media_url;
- subtitle.trackContentType = 'text/vtt';
- subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
- subtitle.name = videoSubtitles[i].name;
- subtitle.language = videoSubtitles[i].lang;
- subtitle.customData = null;
-
- contentSubtitles.push(subtitle);
- }
- }
-
- 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.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.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
- // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
- mediaInfo.tracks = contentSubtitles;
-
- 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.currentTime = shiftCurrentTime(videoProgress?.position); // Set video start position based on the browser video position
- // request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
-
- castSession.loadMedia(request).then(
- function () {
- console.log('media loaded');
- },
- function (error: { code: string }) {
- console.log('Error', error, 'Error code: ' + error.code);
- },
- ); // Send request to cast device
-
- // Do not add videoProgress?.position, this will cause loops!
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [video?.media_url, video?.subtitles, video?.title, video?.vid_thumb_url]);
-
- useEffect(() => {
- // @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) {
- if (isAvailable) {
- setup();
- }
- };
- }, [setup]);
-
- useEffect(() => {
- console.log('isConnected', isConnected);
- if (isConnected) {
- startPlayback();
- }
- }, [isConnected, startPlayback]);
-
- if (!video) {
- return Video for cast not found...
;
- }
-
- return (
- <>
- <>
-
-
-
- {/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
-
- >
- >
- );
-};
-
-export default GoogleCast;
+import { useCallback, useEffect, useState } from 'react';
+import { VideoType } from '../pages/Home';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import updateVideoProgressById from '../api/actions/updateVideoProgressById';
+import watchedThreshold from '../functions/watchedThreshold';
+import { VideoProgressType } from './VideoPlayer';
+
+const getURL = () => {
+ return window.location.origin;
+};
+
+function shiftCurrentTime(contentCurrentTime: number | undefined) {
+ console.log(contentCurrentTime);
+ if (contentCurrentTime === undefined) {
+ return 0;
+ }
+
+ // Shift media back 3 seconds to prevent missing some of the content
+ if (contentCurrentTime > 5) {
+ return contentCurrentTime - 3;
+ } else {
+ return 0;
+ }
+}
+
+async function castVideoProgress(
+ player: {
+ mediaInfo: { contentId: string | string[] };
+ currentTime: number;
+ duration: number;
+ },
+ video: VideoType | undefined,
+) {
+ if (!video) {
+ console.log('castVideoProgress: Video to cast not found...');
+ return;
+ }
+ const videoId = video.youtube_id;
+
+ if (player.mediaInfo.contentId.includes(videoId)) {
+ const currentTime = player.currentTime;
+ const duration = player.duration;
+
+ if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
+ // Check progress every 10 seconds or else progress is checked a few times a second
+ await updateVideoProgressById({
+ youtubeId: videoId,
+ currentProgress: currentTime,
+ });
+
+ if (!video.player.watched) {
+ // Check if video is already marked as watched
+ if (watchedThreshold(currentTime, duration)) {
+ await updateWatchedState({
+ id: videoId,
+ is_watched: true,
+ });
+ }
+ }
+ }
+ }
+}
+
+async function castVideoPaused(
+ player: {
+ currentTime: number;
+ duration: number;
+ mediaInfo: { contentId: string | string[] } | null;
+ },
+ video: VideoType | undefined,
+) {
+ if (!video) {
+ console.log('castVideoPaused: Video to cast not found...');
+ return;
+ }
+
+ const videoId = video?.youtube_id;
+
+ const currentTime = player.currentTime;
+ const duration = player.duration;
+
+ if (player.mediaInfo != null) {
+ if (player.mediaInfo.contentId.includes(videoId)) {
+ if (currentTime !== 0 && duration !== 0) {
+ await updateVideoProgressById({
+ youtubeId: videoId,
+ currentProgress: currentTime,
+ });
+ }
+ }
+ }
+}
+
+type GoogleCastProps = {
+ video?: VideoType;
+ videoProgress?: VideoProgressType;
+ setRefresh?: () => void;
+};
+
+const GoogleCast = ({ video, videoProgress, setRefresh }: GoogleCastProps) => {
+ const [isConnected, setIsConnected] = useState(false);
+
+ const setup = useCallback(() => {
+ const cast = globalThis.cast;
+ const chrome = globalThis.chrome;
+
+ 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.
+ autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
+ });
+
+ const player = new cast.framework.RemotePlayer();
+
+ const playerController = new cast.framework.RemotePlayerController(player);
+
+ // Add event listerner to check if a connection to a cast device is initiated
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
+ function () {
+ setIsConnected(player.isConnected);
+ },
+ );
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
+ function () {
+ castVideoProgress(player, video);
+ },
+ );
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
+ function () {
+ castVideoPaused(player, video);
+ setRefresh?.();
+ },
+ );
+ }, [setRefresh, video]);
+
+ const startPlayback = useCallback(() => {
+ const chrome = globalThis.chrome;
+ const cast = globalThis.cast;
+ const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
+
+ const mediaUrl = video?.media_url;
+ const vidThumbUrl = video?.vid_thumb_url;
+ const contentTitle = video?.title;
+ const contentId = `${getURL()}${mediaUrl}`;
+ const contentImage = `${getURL()}${vidThumbUrl}`;
+ const contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
+
+ const contentSubtitles = [];
+ const videoSubtitles = video?.subtitles; // Array of subtitles
+ if (typeof videoSubtitles !== 'undefined') {
+ for (let i = 0; i < videoSubtitles.length; i++) {
+ const subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
+
+ subtitle.trackContentId = videoSubtitles[i].media_url;
+ subtitle.trackContentType = 'text/vtt';
+ subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
+ subtitle.name = videoSubtitles[i].name;
+ subtitle.language = videoSubtitles[i].lang;
+ subtitle.customData = null;
+
+ contentSubtitles.push(subtitle);
+ }
+ }
+
+ 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.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.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
+ // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
+ mediaInfo.tracks = contentSubtitles;
+
+ 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.currentTime = shiftCurrentTime(videoProgress?.position); // Set video start position based on the browser video position
+ // request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
+
+ castSession.loadMedia(request).then(
+ function () {
+ console.log('media loaded');
+ },
+ function (error: { code: string }) {
+ console.log('Error', error, 'Error code: ' + error.code);
+ },
+ ); // Send request to cast device
+
+ // Do not add videoProgress?.position, this will cause loops!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [video?.media_url, video?.subtitles, video?.title, video?.vid_thumb_url]);
+
+ useEffect(() => {
+ // @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) {
+ if (isAvailable) {
+ setup();
+ }
+ };
+ }, [setup]);
+
+ useEffect(() => {
+ console.log('isConnected', isConnected);
+ if (isConnected) {
+ startPlayback();
+ }
+ }, [isConnected, startPlayback]);
+
+ if (!video) {
+ return Video for cast not found...
;
+ }
+
+ return (
+ <>
+ <>
+
+
+ {/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
+
+ >
+ >
+ );
+};
+
+export default GoogleCast;
diff --git a/frontend/src/components/PaginationDummy.tsx b/frontend/src/components/PaginationDummy.tsx
index e6336e19..e6f80b13 100644
--- a/frontend/src/components/PaginationDummy.tsx
+++ b/frontend/src/components/PaginationDummy.tsx
@@ -1,9 +1,9 @@
-const PaginationDummy = () => {
- return (
-
-
{/** dummy pagination for padding */}
-
- );
-};
-
-export default PaginationDummy;
+const PaginationDummy = () => {
+ return (
+
+
{/** dummy pagination for consistent padding */}
+
+ );
+};
+
+export default PaginationDummy;
diff --git a/frontend/src/components/PlaylistList.tsx b/frontend/src/components/PlaylistList.tsx
index 04fe4db1..daac1718 100644
--- a/frontend/src/components/PlaylistList.tsx
+++ b/frontend/src/components/PlaylistList.tsx
@@ -1,85 +1,87 @@
-import { Link } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import { ViewLayoutType } from '../pages/Home';
-import { PlaylistType } from '../pages/Playlist';
-import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
-import formatDate from '../functions/formatDates';
-import Button from './Button';
-import getApiUrl from '../configuration/getApiUrl';
-
-type PlaylistListProps = {
- playlistList: PlaylistType[] | undefined;
- viewLayout: ViewLayoutType;
- setRefresh: (status: boolean) => void;
-};
-
-const PlaylistList = ({ playlistList, viewLayout, setRefresh }: PlaylistListProps) => {
- if (!playlistList || playlistList.length === 0) {
- return No playlists found.
;
- }
-
- return (
- <>
- {playlistList.map((playlist: PlaylistType) => {
- return (
-
-
-
-
-
-
-
- {playlist.playlist_type != 'custom' && (
-
-
{playlist.playlist_channel}
-
- )}
-
-
-
{playlist.playlist_name}
-
-
-
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
-
- {playlist.playlist_type != 'custom' && (
- <>
- {playlist.playlist_subscribed && (
-
{
- await updatePlaylistSubscription(playlist.playlist_id, false);
-
- setRefresh(true);
- }}
- />
- )}
-
- {!playlist.playlist_subscribed && (
- {
- await updatePlaylistSubscription(playlist.playlist_id, true);
-
- setRefresh(true);
- }}
- />
- )}
- >
- )}
-
-
- );
- })}
- >
- );
-};
-
-export default PlaylistList;
+import { Link } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import { ViewLayoutType } from '../pages/Home';
+import { PlaylistType } from '../pages/Playlist';
+import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
+import formatDate from '../functions/formatDates';
+import Button from './Button';
+import PlaylistThumbnail from './PlaylistThumbnail';
+
+type PlaylistListProps = {
+ playlistList: PlaylistType[] | undefined;
+ viewLayout: ViewLayoutType;
+ setRefresh: (status: boolean) => void;
+};
+
+const PlaylistList = ({ playlistList, viewLayout, setRefresh }: PlaylistListProps) => {
+ if (!playlistList || playlistList.length === 0) {
+ return No playlists found.
;
+ }
+
+ return (
+ <>
+ {playlistList.map((playlist: PlaylistType) => {
+ return (
+
+
+
+ {playlist.playlist_type != 'custom' && (
+
+
{playlist.playlist_channel}
+
+ )}
+
+
+
{playlist.playlist_name}
+
+
+
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
+
+ {playlist.playlist_type != 'custom' && (
+ <>
+ {playlist.playlist_subscribed && (
+
{
+ await updatePlaylistSubscription(playlist.playlist_id, false);
+
+ setRefresh(true);
+ }}
+ />
+ )}
+
+ {!playlist.playlist_subscribed && (
+ {
+ await updatePlaylistSubscription(playlist.playlist_id, true);
+
+ setTimeout(() => {
+ setRefresh(true);
+ }, 500);
+ }}
+ />
+ )}
+ >
+ )}
+
+
+ );
+ })}
+ >
+ );
+};
+
+export default PlaylistList;
diff --git a/frontend/src/components/PlaylistThumbnail.tsx b/frontend/src/components/PlaylistThumbnail.tsx
new file mode 100644
index 00000000..2871e76b
--- /dev/null
+++ b/frontend/src/components/PlaylistThumbnail.tsx
@@ -0,0 +1,22 @@
+import getApiUrl from '../configuration/getApiUrl';
+import defaultPlaylistThumbnail from '/img/default-playlist-thumb.jpg';
+
+type PlaylistThumbnailProps = {
+ playlistId: string;
+ playlistThumbnail: string | undefined;
+};
+
+const PlaylistThumbnail = ({ playlistId, playlistThumbnail }: PlaylistThumbnailProps) => {
+ return (
+ {
+ currentTarget.onerror = null; // prevents looping
+ currentTarget.src = defaultPlaylistThumbnail;
+ }}
+ />
+ );
+};
+
+export default PlaylistThumbnail;
diff --git a/frontend/src/components/VideoListItem.tsx b/frontend/src/components/VideoListItem.tsx
index 9f880bc4..f6deb65d 100644
--- a/frontend/src/components/VideoListItem.tsx
+++ b/frontend/src/components/VideoListItem.tsx
@@ -79,7 +79,8 @@ const VideoListItem = ({
id: video.youtube_id,
is_watched: status,
});
-
+ }}
+ onDone={() => {
refreshVideoList(true);
}}
/>
diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx
index 3f92120a..982cc1ba 100644
--- a/frontend/src/components/VideoPlayer.tsx
+++ b/frontend/src/components/VideoPlayer.tsx
@@ -1,254 +1,268 @@
-import updateVideoProgressById from '../api/actions/updateVideoProgressById';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
-import watchedThreshold from '../functions/watchedThreshold';
-import Notifications from './Notifications';
-import { Dispatch, SetStateAction, SyntheticEvent, useState } from 'react';
-import formatTime from '../functions/formatTime';
-import { useSearchParams } from 'react-router-dom';
-import getApiUrl from '../configuration/getApiUrl';
-
-type VideoTag = SyntheticEvent;
-
-export type SkippedSegmentType = {
- from: number;
- to: number;
-};
-
-export type SponsorSegmentsSkippedType = Record;
-
-type Subtitle = {
- name: string;
- source: string;
- lang: string;
- media_url: string;
-};
-
-type SubtitlesProp = {
- subtitles: Subtitle[];
-};
-
-const Subtitles = ({ subtitles }: SubtitlesProp) => {
- return subtitles.map((subtitle: Subtitle) => {
- let label = subtitle.name;
-
- if (subtitle.source === 'auto') {
- label += ' - auto';
- }
-
- return (
-
- );
- });
-};
-
-const handleTimeUpdate =
- (
- youtubeId: string,
- duration: number,
- watched: boolean,
- sponsorBlock?: SponsorBlockType,
- setSponsorSegmentSkipped?: Dispatch>,
- ) =>
- async (videoTag: VideoTag) => {
- const currentTime = Number(videoTag.currentTarget.currentTime);
-
- if (sponsorBlock && sponsorBlock.segments) {
- sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
- const [from, to] = segment.segment;
-
- if (currentTime >= from && currentTime <= from + 0.3) {
- videoTag.currentTarget.currentTime = to;
-
- setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
- return { ...segments, [segment.UUID]: { from, to } };
- });
- }
-
- if (currentTime > to + 10) {
- setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
- return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
- });
- }
- });
- }
-
- if (currentTime < 10) return;
- if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
- // Check progress every 10 seconds or else progress is checked a few times a second
- await updateVideoProgressById({
- youtubeId,
- currentProgress: currentTime,
- });
-
- if (!watched) {
- // Check if video is already marked as watched
- if (watchedThreshold(currentTime, duration)) {
- await updateWatchedState({
- id: youtubeId,
- is_watched: true,
- });
- }
- }
- }
- };
-
-const handleVideoEnd =
- (
- youtubeId: string,
- watched: boolean,
- setSponsorSegmentSkipped?: Dispatch>,
- ) =>
- async () => {
- if (!watched) {
- // Check if video is already marked as watched
- await updateWatchedState({ id: youtubeId, is_watched: true });
- }
-
- setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
- const keys = Object.keys(segments);
-
- keys.forEach(uuid => {
- segments[uuid] = { from: 0, to: 0 };
- });
-
- return segments;
- });
- };
-
-export type VideoProgressType = {
- youtube_id: string;
- user_id: number;
- position: number;
-};
-
-type VideoPlayerProps = {
- video: VideoResponseType;
- videoProgress?: VideoProgressType;
- sponsorBlock?: SponsorBlockType;
- embed?: boolean;
-};
-
-const VideoPlayer = ({ video, videoProgress, sponsorBlock, embed }: VideoPlayerProps) => {
- const [searchParams] = useSearchParams();
- const searchParamVideoProgress = searchParams.get('t');
-
- const [skippedSegments, setSkippedSegments] = useState({});
-
- const videoId = video.data.youtube_id;
- const videoUrl = video.data.media_url;
- const videoThumbUrl = video.data.vid_thumb_url;
- const watched = video.data.player.watched;
- const duration = video.data.player.duration;
- const videoSubtitles = video.data.subtitles;
-
- let videoSrcProgress = Number(videoProgress?.position) > 0 ? Number(videoProgress?.position) : '';
-
- if (searchParamVideoProgress !== null) {
- videoSrcProgress = searchParamVideoProgress;
- }
-
- const autoplay = false;
-
- return (
- <>
-
-
- {
- localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
- }}
- onLoadStart={(videoTag: VideoTag) => {
- videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume')) ?? 1;
- }}
- onTimeUpdate={handleTimeUpdate(
- videoId,
- duration,
- watched,
- sponsorBlock,
- setSkippedSegments,
- )}
- onPause={async (videoTag: VideoTag) => {
- const currentTime = Number(videoTag.currentTarget.currentTime);
-
- if (currentTime < 10) return;
-
- await updateVideoProgressById({
- youtubeId: videoId,
- currentProgress: currentTime,
- });
- }}
- onEnded={handleVideoEnd(videoId, watched)}
- autoPlay={autoplay}
- controls
- width="100%"
- playsInline
- id="video-item"
- >
-
- {videoSubtitles && }
-
-
-
-
-
-
- >
- );
-};
-
-export default VideoPlayer;
+import updateVideoProgressById from '../api/actions/updateVideoProgressById';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
+import watchedThreshold from '../functions/watchedThreshold';
+import Notifications from './Notifications';
+import { Dispatch, SetStateAction, SyntheticEvent, useState } from 'react';
+import formatTime from '../functions/formatTime';
+import { useSearchParams } from 'react-router-dom';
+import getApiUrl from '../configuration/getApiUrl';
+
+type VideoTag = SyntheticEvent;
+
+export type SkippedSegmentType = {
+ from: number;
+ to: number;
+};
+
+export type SponsorSegmentsSkippedType = Record;
+
+type Subtitle = {
+ name: string;
+ source: string;
+ lang: string;
+ media_url: string;
+};
+
+type SubtitlesProp = {
+ subtitles: Subtitle[];
+};
+
+const Subtitles = ({ subtitles }: SubtitlesProp) => {
+ return subtitles.map((subtitle: Subtitle) => {
+ let label = subtitle.name;
+
+ if (subtitle.source === 'auto') {
+ label += ' - auto';
+ }
+
+ return (
+
+ );
+ });
+};
+
+const handleTimeUpdate =
+ (
+ youtubeId: string,
+ duration: number,
+ watched: boolean,
+ sponsorBlock?: SponsorBlockType,
+ setSponsorSegmentSkipped?: Dispatch>,
+ ) =>
+ async (videoTag: VideoTag) => {
+ const currentTime = Number(videoTag.currentTarget.currentTime);
+
+ if (sponsorBlock && sponsorBlock.segments) {
+ sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
+ const [from, to] = segment.segment;
+
+ if (currentTime >= from && currentTime <= from + 0.3) {
+ videoTag.currentTarget.currentTime = to;
+
+ setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
+ return { ...segments, [segment.UUID]: { from, to } };
+ });
+ }
+
+ if (currentTime > to + 10) {
+ setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
+ return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
+ });
+ }
+ });
+ }
+
+ if (currentTime < 10) return;
+ if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
+ // Check progress every 10 seconds or else progress is checked a few times a second
+ await updateVideoProgressById({
+ youtubeId,
+ currentProgress: currentTime,
+ });
+
+ if (!watched) {
+ // Check if video is already marked as watched
+ if (watchedThreshold(currentTime, duration)) {
+ await updateWatchedState({
+ id: youtubeId,
+ is_watched: true,
+ });
+ }
+ }
+ }
+ };
+
+export type VideoProgressType = {
+ youtube_id: string;
+ user_id: number;
+ position: number;
+};
+
+type VideoPlayerProps = {
+ video: VideoResponseType;
+ videoProgress?: VideoProgressType;
+ sponsorBlock?: SponsorBlockType;
+ embed?: boolean;
+ autoplay?: boolean;
+ onVideoEnd?: () => void;
+};
+
+const VideoPlayer = ({
+ video,
+ videoProgress,
+ sponsorBlock,
+ embed,
+ autoplay = false,
+ onVideoEnd,
+}: VideoPlayerProps) => {
+ const [searchParams] = useSearchParams();
+ const searchParamVideoProgress = searchParams.get('t');
+
+ const [skippedSegments, setSkippedSegments] = useState({});
+
+ const videoId = video.data.youtube_id;
+ const videoUrl = video.data.media_url;
+ const videoThumbUrl = video.data.vid_thumb_url;
+ const watched = video.data.player.watched;
+ const duration = video.data.player.duration;
+ const videoSubtitles = video.data.subtitles;
+
+ let videoSrcProgress = Number(videoProgress?.position) > 0 ? Number(videoProgress?.position) : '';
+
+ if (searchParamVideoProgress !== null) {
+ videoSrcProgress = searchParamVideoProgress;
+ }
+
+ const handleVideoEnd =
+ (
+ youtubeId: string,
+ watched: boolean,
+ setSponsorSegmentSkipped?: Dispatch>,
+ ) =>
+ async () => {
+ if (!watched) {
+ // Check if video is already marked as watched
+ await updateWatchedState({ id: youtubeId, is_watched: true });
+ }
+
+ setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
+ const keys = Object.keys(segments);
+
+ keys.forEach(uuid => {
+ segments[uuid] = { from: 0, to: 0 };
+ });
+
+ return segments;
+ });
+
+ onVideoEnd?.();
+ };
+
+ return (
+ <>
+
+
+ {
+ localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
+ }}
+ onRateChange={(videoTag: VideoTag) => {
+ localStorage.setItem('playerSpeed', videoTag.currentTarget.playbackRate.toString());
+ }}
+ onLoadStart={(videoTag: VideoTag) => {
+ videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume')) ?? 1;
+ videoTag.currentTarget.playbackRate =
+ Number(localStorage.getItem('playerSpeed')) ?? 1;
+ }}
+ onTimeUpdate={handleTimeUpdate(
+ videoId,
+ duration,
+ watched,
+ sponsorBlock,
+ setSkippedSegments,
+ )}
+ onPause={async (videoTag: VideoTag) => {
+ const currentTime = Number(videoTag.currentTarget.currentTime);
+
+ if (currentTime < 10) return;
+
+ await updateVideoProgressById({
+ youtubeId: videoId,
+ currentProgress: currentTime,
+ });
+ }}
+ onEnded={handleVideoEnd(videoId, watched)}
+ autoPlay={autoplay}
+ controls
+ width="100%"
+ playsInline
+ id="video-item"
+ >
+
+ {videoSubtitles && }
+
+
+
+
+
+
+ >
+ );
+};
+
+export default VideoPlayer;
diff --git a/frontend/src/components/WatchedCheckBox.tsx b/frontend/src/components/WatchedCheckBox.tsx
index c5a5d012..490bb3c3 100644
--- a/frontend/src/components/WatchedCheckBox.tsx
+++ b/frontend/src/components/WatchedCheckBox.tsx
@@ -1,33 +1,63 @@
import iconUnseen from '/img/icon-unseen.svg';
import iconSeen from '/img/icon-seen.svg';
+import { useEffect, useState } from 'react';
type WatchedCheckBoxProps = {
watched: boolean;
onClick?: (status: boolean) => void;
+ onDone?: (status: boolean) => void;
};
-const WatchedCheckBox = ({ watched, onClick }: WatchedCheckBoxProps) => {
+const WatchedCheckBox = ({ watched, onClick, onDone }: WatchedCheckBoxProps) => {
+ const [loading, setLoading] = useState(false);
+ const [state, setState] = useState(false);
+
+ useEffect(() => {
+ if (loading) {
+ onClick?.(state);
+
+ const timeout = setTimeout(() => {
+ onDone?.(state);
+ setLoading(false);
+ }, 1000);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [loading]);
+
return (
<>
- {watched && (
+ {loading && (
+ <>
+
+ >
+ )}
+ {!loading && watched && (
{
- onClick?.(false);
+ setState(false);
+ setLoading(true);
}}
/>
)}
- {!watched && (
+ {!loading && !watched && (
{
- onClick?.(true);
+ setState(true);
+ setLoading(true);
}}
/>
)}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 5f060daf..719da032 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,314 +1,316 @@
-import * as React from 'react';
-import * as ReactDOM from 'react-dom/client';
-import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom';
-import Routes from './configuration/routes/RouteList';
-import './style.css';
-import Base from './pages/Base';
-import About from './pages/About';
-import Channels from './pages/Channels';
-import ErrorPage from './pages/ErrorPage';
-import Home from './pages/Home';
-import Logout from './pages/Logout';
-import Playlist from './pages/Playlist';
-import Playlists from './pages/Playlists';
-import Search from './pages/Search';
-import SettingsDashboard from './pages/SettingsDashboard';
-import Video from './pages/Video';
-import Login from './pages/Login';
-import SettingsActions from './pages/SettingsActions';
-import SettingsApplication from './pages/SettingsApplication';
-import SettingsScheduling from './pages/SettingsScheduling';
-import SettingsUser from './pages/SettingsUser';
-import loadUserMeConfig from './api/loader/loadUserConfig';
-import loadAuth from './api/loader/loadAuth';
-import ChannelBase from './pages/ChannelBase';
-import ChannelVideo from './pages/ChannelVideo';
-import ChannelPlaylist from './pages/ChannelPlaylist';
-import ChannelAbout from './pages/ChannelAbout';
-import ChannelStream from './pages/ChannelStream';
-import Download from './pages/Download';
-import ChannelShorts from './pages/ChannelShorts';
-
-const router = createBrowserRouter(
- [
- {
- path: Routes.Home,
- loader: async () => {
- console.log('------------ after reload');
-
- const auth = await loadAuth();
- if (auth.status === 403) {
- return redirect(Routes.Login);
- }
-
- const authData = await auth.json();
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig, auth: authData };
- },
- element: ,
- errorElement: ,
- children: [
- {
- index: true,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.Video(':videoId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.Channels,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.Channel(':channelId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- children: [
- {
- index: true,
- path: Routes.ChannelVideo(':channelId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.ChannelStream(':channelId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.ChannelShorts(':channelId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.ChannelPlaylist(':channelId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.ChannelAbout(':channelId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- ],
- },
- {
- path: Routes.Playlists,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.Playlist(':playlistId'),
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.Downloads,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.Search,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.SettingsDashboard,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.SettingsActions,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.SettingsApplication,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.SettingsScheduling,
- element: ,
- loader: async () => {
- const authResponse = await loadAuth();
- if (authResponse.status === 403) {
- return redirect(Routes.Login);
- }
-
- return {};
- },
- },
- {
- path: Routes.SettingsUser,
- element: ,
- loader: async () => {
- const auth = await loadAuth();
- if (auth.status === 403) {
- return redirect(Routes.Login);
- }
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig };
- },
- },
- {
- path: Routes.About,
- element: ,
- },
- ],
- },
- {
- path: Routes.Login,
- element: ,
- errorElement: ,
- },
- {
- path: Routes.Logout,
- element: ,
- errorElement: ,
- },
- ],
- { basename: import.meta.env.BASE_URL },
-);
-
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
- ,
-);
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom';
+import Routes from './configuration/routes/RouteList';
+import './style.css';
+import Base from './pages/Base';
+import About from './pages/About';
+import Channels from './pages/Channels';
+import ErrorPage from './pages/ErrorPage';
+import Home from './pages/Home';
+import Logout from './pages/Logout';
+import Playlist from './pages/Playlist';
+import Playlists from './pages/Playlists';
+import Search from './pages/Search';
+import SettingsDashboard from './pages/SettingsDashboard';
+import Video from './pages/Video';
+import Login from './pages/Login';
+import SettingsActions from './pages/SettingsActions';
+import SettingsApplication from './pages/SettingsApplication';
+import SettingsScheduling from './pages/SettingsScheduling';
+import SettingsUser from './pages/SettingsUser';
+import loadUserMeConfig from './api/loader/loadUserConfig';
+import loadAuth from './api/loader/loadAuth';
+import ChannelBase from './pages/ChannelBase';
+import ChannelVideo from './pages/ChannelVideo';
+import ChannelPlaylist from './pages/ChannelPlaylist';
+import ChannelAbout from './pages/ChannelAbout';
+import Download from './pages/Download';
+
+const router = createBrowserRouter(
+ [
+ {
+ path: Routes.Home,
+ loader: async () => {
+ console.log('------------ after reload');
+
+ const auth = await loadAuth();
+ if (auth.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const authData = await auth.json();
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig, auth: authData };
+ },
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.Video(':videoId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ },
+ {
+ path: Routes.Channels,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.Channel(':channelId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ children: [
+ {
+ index: true,
+ path: Routes.ChannelVideo(':channelId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.ChannelStream(':channelId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.ChannelShorts(':channelId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.ChannelPlaylist(':channelId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.ChannelAbout(':channelId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ },
+ ],
+ },
+ {
+ path: Routes.Playlists,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.Playlist(':playlistId'),
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.Downloads,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.Search,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.SettingsDashboard,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ },
+ {
+ path: Routes.SettingsActions,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ },
+ {
+ path: Routes.SettingsApplication,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ },
+ {
+ path: Routes.SettingsScheduling,
+ element: ,
+ loader: async () => {
+ const authResponse = await loadAuth();
+ if (authResponse.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ return {};
+ },
+ },
+ {
+ path: Routes.SettingsUser,
+ element: ,
+ loader: async () => {
+ const auth = await loadAuth();
+ if (auth.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig };
+ },
+ },
+ {
+ path: Routes.About,
+ element: ,
+ },
+ ],
+ },
+ {
+ path: Routes.Login,
+ element: ,
+ errorElement: ,
+ },
+ {
+ path: Routes.Logout,
+ element: ,
+ errorElement: ,
+ },
+ ],
+ { basename: import.meta.env.BASE_URL },
+);
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index 7a73c244..aac50539 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -1,64 +1,60 @@
-import { Helmet } from 'react-helmet';
-
-const About = () => {
- return (
- <>
-
- TA | About
-
-
-
-
-
About The Tube Archivist
-
-
-
Useful Links
-
- This project is in active and constant development, take a look at the{' '}
-
- roadmap
- {' '}
- for a overview.
-
-
- All functionality is documented in our up-to-date{' '}
-
- user guide
-
- .
-
-
- All contributions are welcome: Open an{' '}
-
- issue
- {' '}
- for any bugs and errors, join us on{' '}
-
- Discord
- {' '}
- to discuss details. The{' '}
-
- contributing
- {' '}
- page is a good place to get started.
-
-
-
-
Donate
-
- Here are{' '}
-
- some links
-
- , if you want to buy the developer a coffee. Thank you for your support!
-
-
-
- >
- );
-};
-
-export default About;
+const About = () => {
+ return (
+ <>
+ TA | About
+
+
+
+
About The Tube Archivist
+
+
+
Useful Links
+
+ This project is in active and constant development, take a look at the{' '}
+
+ roadmap
+ {' '}
+ for a overview.
+
+
+ All functionality is documented in our up-to-date{' '}
+
+ user guide
+
+ .
+
+
+ All contributions are welcome: Open an{' '}
+
+ issue
+ {' '}
+ for any bugs and errors, join us on{' '}
+
+ Discord
+ {' '}
+ to discuss details. The{' '}
+
+ contributing
+ {' '}
+ page is a good place to get started.
+
+
+
+
Donate
+
+ Here are{' '}
+
+ some links
+
+ , if you want to buy the developer a coffee. Thank you for your support!
+
+
+
+ >
+ );
+};
+
+export default About;
diff --git a/frontend/src/pages/ChannelAbout.tsx b/frontend/src/pages/ChannelAbout.tsx
index 1b7e752b..4123a063 100644
--- a/frontend/src/pages/ChannelAbout.tsx
+++ b/frontend/src/pages/ChannelAbout.tsx
@@ -1,350 +1,444 @@
-import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
-import ChannelOverview from '../components/ChannelOverview';
-import { useEffect, useState } from 'react';
-import loadChannelById from '../api/loader/loadChannelById';
-import { ChannelResponseType } from './ChannelBase';
-import Linkify from '../components/Linkify';
-import deleteChannel from '../api/actions/deleteChannel';
-import Routes from '../configuration/routes/RouteList';
-import queueReindex, { ReindexType, ReindexTypeEnum } from '../api/actions/queueReindex';
-import formatDate from '../functions/formatDates';
-import PaginationDummy from '../components/PaginationDummy';
-import FormattedNumber from '../components/FormattedNumber';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-
-const handleSponsorBlockIntegrationOverwrite = (integration: boolean | undefined) => {
- if (integration === undefined) {
- return 'False';
- }
-
- if (integration) {
- return integration;
- } else {
- return 'Disabled';
- }
-};
-
-export type ChannelBaseOutletContextType = {
- isAdmin: boolean;
- currentPage: number;
- setCurrentPage: (page: number) => void;
- startNotification: boolean;
- setStartNotification: (status: boolean) => void;
-};
-
-export type OutletContextType = {
- isAdmin: boolean;
- currentPage: number;
- setCurrentPage: (page: number) => void;
-};
-
-type ChannelAboutParams = {
- channelId: string;
-};
-
-const ChannelAbout = () => {
- const { channelId } = useParams() as ChannelAboutParams;
- const { isAdmin, setStartNotification } = useOutletContext() as ChannelBaseOutletContextType;
- const navigate = useNavigate();
-
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
- const [reindex, setReindex] = useState(false);
- const [refresh, setRefresh] = useState(false);
-
- const [channelResponse, setChannelResponse] = useState();
-
- const channel = channelResponse?.data;
-
- const channelOverwrites = channel?.channel_overwrites;
-
- const handleSubmit = async (event: { preventDefault: () => void }) => {
- event.preventDefault();
-
- //TODO: implement request to about api endpoint ( when implemented )
- // `/api/channel/${channel.channel_id}/about/`
- };
-
- useEffect(() => {
- (async () => {
- const channelResponse = await loadChannelById(channelId);
-
- setChannelResponse(channelResponse);
- setRefresh(false);
- })();
- }, [refresh, channelId]);
-
- if (!channel) {
- return 'Channel not found!';
- }
-
- return (
- <>
-
- TA | Channel: About {channel.channel_name}
-
-
-
-
-
-
-
-
Last refreshed: {formatDate(channel.channel_last_refresh)}
- {channel.channel_active && (
-
- Youtube:{' '}
-
- Active
-
-
- )}
- {!channel.channel_active &&
Youtube: Deactivated
}
-
-
-
-
-
- {channel.channel_views > 0 && (
-
- )}
-
- {isAdmin && (
- <>
-
- {!showDeleteConfirm && (
-
setShowDeleteConfirm(!showDeleteConfirm)}
- />
- )}
-
- {showDeleteConfirm && (
-
- Delete {channel.channel_name} including all videos?
- {
- await deleteChannel(channelId);
- navigate(Routes.Channels);
- }}
- />{' '}
- setShowDeleteConfirm(!showDeleteConfirm)}
- />
-
- )}
-
- {reindex &&
Reindex scheduled
}
- {!reindex && (
-
- {
- await queueReindex(channelId, ReindexTypeEnum.channel as ReindexType);
-
- setReindex(true);
- setStartNotification(true);
- }}
- />{' '}
- {
- await queueReindex(
- channelId,
- ReindexTypeEnum.channel as ReindexType,
- true,
- );
-
- setReindex(true);
- setStartNotification(true);
- }}
- />
-
- )}
- >
- )}
-
-
-
-
- {channel.channel_description && (
-
-
- {channel.channel_description}
-
-
-
setDescriptionExpanded(!descriptionExpanded)}
- />
-
- )}
-
- {channel.channel_tags && (
-
-
- {channel.channel_tags.map(tag => {
- return (
-
- {tag}
-
- );
- })}
-
-
- )}
-
- {isAdmin && (
-
- )}
-
-
-
- >
- );
-};
-
-export default ChannelAbout;
+import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
+import ChannelOverview from '../components/ChannelOverview';
+import { useEffect, useState } from 'react';
+import loadChannelById from '../api/loader/loadChannelById';
+import { ChannelResponseType } from './ChannelBase';
+import Linkify from '../components/Linkify';
+import deleteChannel from '../api/actions/deleteChannel';
+import Routes from '../configuration/routes/RouteList';
+import queueReindex, { ReindexType, ReindexTypeEnum } from '../api/actions/queueReindex';
+import formatDate from '../functions/formatDates';
+import PaginationDummy from '../components/PaginationDummy';
+import FormattedNumber from '../components/FormattedNumber';
+import Button from '../components/Button';
+import updateChannelSettings, {
+ ChannelAboutConfigType,
+} from '../api/actions/updateChannelSettings';
+
+const toStringToBool = (str: string) => {
+ try {
+ return JSON.parse(str);
+ } catch {
+ return null;
+ }
+};
+
+export type ChannelBaseOutletContextType = {
+ isAdmin: boolean;
+ currentPage: number;
+ setCurrentPage: (page: number) => void;
+ startNotification: boolean;
+ setStartNotification: (status: boolean) => void;
+};
+
+export type OutletContextType = {
+ isAdmin: boolean;
+ currentPage: number;
+ setCurrentPage: (page: number) => void;
+};
+
+type ChannelAboutParams = {
+ channelId: string;
+};
+
+const ChannelAbout = () => {
+ const { channelId } = useParams() as ChannelAboutParams;
+ const { isAdmin, setStartNotification } = useOutletContext() as ChannelBaseOutletContextType;
+ const navigate = useNavigate();
+
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [descriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [reindex, setReindex] = useState(false);
+ const [refresh, setRefresh] = useState(true);
+
+ const [channelResponse, setChannelResponse] = useState();
+ const [channelConfig, setChannelConfig] = useState();
+
+ const [downloadFormat, setDownloadFormat] = useState(channelConfig?.download_format);
+ const [autoDeleteAfter, setAutoDeleteAfter] = useState(channelConfig?.autodelete_days);
+ const [indexPlaylists, setIndexPlaylists] = useState(
+ channelConfig?.index_playlists ? 'true' : 'false',
+ );
+ const [enableSponsorblock, setEnableSponsorblock] = useState(
+ channelConfig?.integrate_sponsorblock,
+ );
+ const [pageSizeVideo, setPageSizeVideo] = useState(channelConfig?.subscriptions_channel_size);
+ const [pageSizeStreams, setPageSizeStreams] = useState(
+ channelConfig?.subscriptions_live_channel_size,
+ );
+ const [pageSizeShorts, setPageSizeShorts] = useState(
+ channelConfig?.subscriptions_shorts_channel_size,
+ );
+
+ const channel = channelResponse?.data;
+
+ const channelOverwrites = channel?.channel_overwrites;
+
+ const handleSubmit = async (event: { preventDefault: () => void }) => {
+ event.preventDefault();
+
+ await updateChannelSettings(channelId, {
+ index_playlists: toStringToBool(indexPlaylists),
+ download_format: downloadFormat,
+ autodelete_days: autoDeleteAfter,
+ integrate_sponsorblock: enableSponsorblock,
+ subscriptions_channel_size: pageSizeVideo,
+ subscriptions_live_channel_size: pageSizeStreams,
+ subscriptions_shorts_channel_size: pageSizeShorts,
+ });
+
+ setRefresh(true);
+ };
+
+ useEffect(() => {
+ (async () => {
+ if (refresh) {
+ const channelResponse = await loadChannelById(channelId);
+
+ setChannelResponse(channelResponse);
+ setChannelConfig(channelResponse?.data?.channel_overwrites);
+ console.log('channel_overwrites', channelResponse?.data);
+ console.log('channel_overwrites', channelResponse?.data?.channel_overwrites);
+ console.log('channel_overwrites', '--------');
+ setRefresh(false);
+ }
+ })();
+ }, [refresh, channelId]);
+
+ if (!channel) {
+ return 'Channel not found!';
+ }
+
+ return (
+ <>
+ {`TA | Channel: About ${channel.channel_name}`}
+
+
+
+
+
+
+
Last refreshed: {formatDate(channel.channel_last_refresh)}
+ {channel.channel_active && (
+
+ Youtube:{' '}
+
+ Active
+
+
+ )}
+ {!channel.channel_active &&
Youtube: Deactivated
}
+
+
+
+
+
+ {channel.channel_views > 0 && (
+
+ )}
+
+ {isAdmin && (
+ <>
+
+ {!showDeleteConfirm && (
+
setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+ )}
+
+ {showDeleteConfirm && (
+
+ Delete {channel.channel_name} including all videos?
+ {
+ await deleteChannel(channelId);
+ navigate(Routes.Channels);
+ }}
+ />{' '}
+ setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+
+ )}
+
+ {reindex &&
Reindex scheduled
}
+ {!reindex && (
+
+ {
+ await queueReindex(channelId, ReindexTypeEnum.channel as ReindexType);
+
+ setReindex(true);
+ setStartNotification(true);
+ }}
+ />{' '}
+ {
+ await queueReindex(
+ channelId,
+ ReindexTypeEnum.channel as ReindexType,
+ true,
+ );
+
+ setReindex(true);
+ setStartNotification(true);
+ }}
+ />
+
+ )}
+ >
+ )}
+
+
+
+
+ {channel.channel_description && (
+
+
+ {channel.channel_description}
+
+
+
setDescriptionExpanded(!descriptionExpanded)}
+ />
+
+ )}
+
+ {channel.channel_tags && (
+
+
+ {channel.channel_tags.map(tag => {
+ return (
+
+ {tag}
+
+ );
+ })}
+
+
+ )}
+
+ {isAdmin && (
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default ChannelAbout;
diff --git a/frontend/src/pages/ChannelBase.tsx b/frontend/src/pages/ChannelBase.tsx
index 45f23457..c8d97946 100644
--- a/frontend/src/pages/ChannelBase.tsx
+++ b/frontend/src/pages/ChannelBase.tsx
@@ -1,99 +1,104 @@
-import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import { ChannelType } from './Channels';
-import { ConfigType } from './Home';
-import { OutletContextType } from './Base';
-import Notifications from '../components/Notifications';
-import { useEffect, useState } from 'react';
-import ChannelBanner from '../components/ChannelBanner';
-import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
-
-type ChannelParams = {
- channelId: string;
-};
-
-export type ChannelResponseType = {
- data: ChannelType;
- config: ConfigType;
-};
-
-const ChannelBase = () => {
- const { channelId } = useParams() as ChannelParams;
- const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
-
- const [channelNav, setChannelNav] = useState();
- const [startNotification, setStartNotification] = useState(false);
-
- const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
-
- useEffect(() => {
- (async () => {
- const channelNavResponse = await loadChannelNav(channelId);
-
- setChannelNav(channelNavResponse);
- })();
- }, [channelId]);
-
- if (!channelId) {
- return [];
- }
-
- return (
- <>
-
-
-
-
-
-
-
-
-
Videos
-
- {has_streams && (
-
- Streams
-
- )}
- {has_shorts && (
-
- Shorts
-
- )}
- {has_playlists && (
-
- Playlists
-
- )}
-
- About
-
- {has_pending && isAdmin && (
-
- Downloads
-
- )}
-
-
-
setStartNotification(false)}
- />
-
-
-
- >
- );
-};
-
-export default ChannelBase;
+import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import { ChannelType } from './Channels';
+import { ConfigType } from './Home';
+import { OutletContextType } from './Base';
+import Notifications from '../components/Notifications';
+import { useEffect, useState } from 'react';
+import ChannelBanner from '../components/ChannelBanner';
+import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
+import loadChannelById from '../api/loader/loadChannelById';
+
+type ChannelParams = {
+ channelId: string;
+};
+
+export type ChannelResponseType = {
+ data: ChannelType;
+ config: ConfigType;
+};
+
+const ChannelBase = () => {
+ const { channelId } = useParams() as ChannelParams;
+ const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+
+ const [channelResponse, setChannelResponse] = useState();
+ const [channelNav, setChannelNav] = useState();
+ const [startNotification, setStartNotification] = useState(false);
+
+ const channel = channelResponse?.data;
+ const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
+
+ useEffect(() => {
+ (async () => {
+ const channelNavResponse = await loadChannelNav(channelId);
+ const channelResponse = await loadChannelById(channelId);
+
+ setChannelResponse(channelResponse);
+ setChannelNav(channelNavResponse);
+ })();
+ }, [channelId]);
+
+ if (!channelId) {
+ return [];
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
Videos
+
+ {has_streams && (
+
+ Streams
+
+ )}
+ {has_shorts && (
+
+ Shorts
+
+ )}
+ {has_playlists && (
+
+ Playlists
+
+ )}
+
+ About
+
+ {has_pending && isAdmin && (
+
+ Downloads
+
+ )}
+
+
+
setStartNotification(false)}
+ />
+
+
+
+ >
+ );
+};
+
+export default ChannelBase;
diff --git a/frontend/src/pages/ChannelPlaylist.tsx b/frontend/src/pages/ChannelPlaylist.tsx
index 15a8c8e4..050bafc2 100644
--- a/frontend/src/pages/ChannelPlaylist.tsx
+++ b/frontend/src/pages/ChannelPlaylist.tsx
@@ -1,122 +1,119 @@
-import { useLoaderData, useOutletContext, useParams } from 'react-router-dom';
-import Notifications from '../components/Notifications';
-import PlaylistList from '../components/PlaylistList';
-import { ViewLayoutType } from './Home';
-import { ViewStyles } from '../configuration/constants/ViewStyle';
-import { useEffect, useState } from 'react';
-import { OutletContextType } from './Base';
-import Pagination from '../components/Pagination';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import { Helmet } from 'react-helmet';
-import loadPlaylistList from '../api/loader/loadPlaylistList';
-import { PlaylistsResponseType } from './Playlists';
-import iconGridView from '/img/icon-gridview.svg';
-import iconListView from '/img/icon-listview.svg';
-import { UserMeType } from '../api/actions/updateUserConfig';
-
-type ChannelPlaylistLoaderDataType = {
- userConfig: UserMeType;
-};
-
-const ChannelPlaylist = () => {
- const { channelId } = useParams();
- const { userConfig } = useLoaderData() as ChannelPlaylistLoaderDataType;
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
-
- const userMeConfig = userConfig.config;
-
- const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false);
- const [view, setView] = useState(userMeConfig.view_style_playlist || 'grid');
- const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
- const [refreshPlaylists, setRefreshPlaylists] = useState(false);
-
- const [playlistsResponse, setPlaylistsResponse] = useState();
-
- const playlistList = playlistsResponse?.data;
- const pagination = playlistsResponse?.paginate;
-
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- const playlists = await loadPlaylistList({
- channel: channelId,
- subscribed: showSubedOnly,
- });
-
- setPlaylistsResponse(playlists);
- setRefreshPlaylists(false);
- })();
- }, [channelId, refreshPlaylists, showSubedOnly, currentPage]);
-
- return (
- <>
-
- TA | Channel: Playlists
-
-
-
-
-
-
-
-
Show subscribed only:
-
- {
- setShowSubedOnly(!showSubedOnly);
- }}
- type="checkbox"
- />
- {!showSubedOnly && (
-
- Off
-
- )}
- {showSubedOnly && (
-
- On
-
- )}
-
-
-
-
{
- setView('grid');
- }}
- alt="grid view"
- />
-
{
- setView('list');
- }}
- alt="list view"
- />
-
-
-
-
-
-
-
- >
- );
-};
-
-export default ChannelPlaylist;
+import { useLoaderData, useOutletContext, useParams } from 'react-router-dom';
+import Notifications from '../components/Notifications';
+import PlaylistList from '../components/PlaylistList';
+import { ViewLayoutType } from './Home';
+import { ViewStyles } from '../configuration/constants/ViewStyle';
+import { useEffect, useState } from 'react';
+import { OutletContextType } from './Base';
+import Pagination from '../components/Pagination';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import loadPlaylistList from '../api/loader/loadPlaylistList';
+import { PlaylistsResponseType } from './Playlists';
+import iconGridView from '/img/icon-gridview.svg';
+import iconListView from '/img/icon-listview.svg';
+import { UserMeType } from '../api/actions/updateUserConfig';
+
+type ChannelPlaylistLoaderDataType = {
+ userConfig: UserMeType;
+};
+
+const ChannelPlaylist = () => {
+ const { channelId } = useParams();
+ const { userConfig } = useLoaderData() as ChannelPlaylistLoaderDataType;
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+
+ const userMeConfig = userConfig.config;
+
+ const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false);
+ const [view, setView] = useState(userMeConfig.view_style_playlist || 'grid');
+ const [gridItems] = useState(userMeConfig.grid_items || 3);
+ const [refreshPlaylists, setRefreshPlaylists] = useState(false);
+
+ const [playlistsResponse, setPlaylistsResponse] = useState();
+
+ const playlistList = playlistsResponse?.data;
+ const pagination = playlistsResponse?.paginate;
+
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ const playlists = await loadPlaylistList({
+ channel: channelId,
+ subscribed: showSubedOnly,
+ });
+
+ setPlaylistsResponse(playlists);
+ setRefreshPlaylists(false);
+ })();
+ }, [channelId, refreshPlaylists, showSubedOnly, currentPage]);
+
+ return (
+ <>
+ TA | Channel: Playlists
+
+
+
+
+
+
+
Show subscribed only:
+
+ {
+ setShowSubedOnly(!showSubedOnly);
+ }}
+ type="checkbox"
+ />
+ {!showSubedOnly && (
+
+ Off
+
+ )}
+ {showSubedOnly && (
+
+ On
+
+ )}
+
+
+
+
{
+ setView('grid');
+ }}
+ alt="grid view"
+ />
+
{
+ setView('list');
+ }}
+ alt="list view"
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ChannelPlaylist;
diff --git a/frontend/src/pages/ChannelShorts.tsx b/frontend/src/pages/ChannelShorts.tsx
deleted file mode 100644
index 3fa5635d..00000000
--- a/frontend/src/pages/ChannelShorts.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Helmet } from 'react-helmet';
-import { useParams } from 'react-router-dom';
-
-const ChannelShorts = () => {
- const { channelId } = useParams();
-
- return (
- <>
-
- TA | Channel: {channel.channel_name}
-
- >
- );
-};
-
-export default ChannelShorts;
diff --git a/frontend/src/pages/ChannelStream.tsx b/frontend/src/pages/ChannelStream.tsx
deleted file mode 100644
index 4fa352a2..00000000
--- a/frontend/src/pages/ChannelStream.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Helmet } from 'react-helmet';
-import { useParams } from 'react-router-dom';
-
-const ChannelStream = () => {
- const { channelId } = useParams();
-
- return (
- <>
-
- TA | Channel: {channel.channel_name}
-
- >
- );
-};
-
-export default ChannelStream;
diff --git a/frontend/src/pages/ChannelVideo.tsx b/frontend/src/pages/ChannelVideo.tsx
index 815cb9be..a8ab7674 100644
--- a/frontend/src/pages/ChannelVideo.tsx
+++ b/frontend/src/pages/ChannelVideo.tsx
@@ -1,211 +1,216 @@
-import { useEffect, useState } from 'react';
-import {
- Link,
- useLoaderData,
- useOutletContext,
- useParams,
- useSearchParams,
-} from 'react-router-dom';
-import { SortByType, SortOrderType, ViewLayoutType } from './Home';
-import { OutletContextType } from './Base';
-import { UserMeType } from '../api/actions/updateUserConfig';
-import VideoList from '../components/VideoList';
-import Routes from '../configuration/routes/RouteList';
-import Pagination from '../components/Pagination';
-import Filterbar from '../components/Filterbar';
-import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
-import ChannelOverview from '../components/ChannelOverview';
-import loadChannelById from '../api/loader/loadChannelById';
-import { ChannelResponseType } from './ChannelBase';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-import loadVideoListByFilter, {
- VideoListByFilterResponseType,
-} from '../api/loader/loadVideoListByPage';
-
-type ChannelParams = {
- channelId: string;
-};
-
-type ChannelVideoLoaderType = {
- userConfig: UserMeType;
-};
-
-const ChannelVideo = () => {
- const { channelId } = useParams() as ChannelParams;
- const { userConfig } = useLoaderData() as ChannelVideoLoaderType;
- const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
-
- const userMeConfig = userConfig.config;
-
- const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
- const [sortBy, setSortBy] = useState(userMeConfig.sort_by || 'published');
- const [sortOrder, setSortOrder] = useState(userMeConfig.sort_order || 'asc');
- const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
- const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
- const [refresh, setRefresh] = useState(false);
-
- const [channelResponse, setChannelResponse] = useState();
- const [videoResponse, setVideoReponse] = useState();
-
- const channel = channelResponse?.data;
- const videoList = videoResponse?.data;
- const pagination = videoResponse?.paginate;
-
- const hasVideos = videoResponse?.data?.length !== 0;
- const showEmbeddedVideo = videoId !== null;
-
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- if (
- refresh ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const channelResponse = await loadChannelById(channelId);
- const videos = await loadVideoListByFilter({
- channel: channelId,
- page: currentPage,
- watch: hideWatched ? 'unwatched' : undefined,
- sort: sortBy,
- order: sortOrder,
- });
-
- setChannelResponse(channelResponse);
- setVideoReponse(videos);
- setRefresh(false);
- }
- })();
- // Do not add sort, order, hideWatched this will not work as expected!
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [refresh, currentPage, channelId, pagination?.current_page]);
-
- const aggs = {
- total_items: { value: '' },
- total_duration: { value_str: '' },
- total_size: { value: '' },
- };
-
- if (!channel) {
- return (
-
-
-
Channel {channelId} not found!
-
- );
- }
-
- return (
- <>
-
- TA | Channel: {channel.channel_name}
-
-
-
-
-
-
- {aggs && (
- <>
-
- {aggs.total_items.value} videos | {' '}
- {aggs.total_duration.value_str} playback | {' '}
- Total size {aggs.total_size.value}
-
-
- {
- await updateWatchedState({
- id: channel.channel_id,
- is_watched: true,
- });
-
- setRefresh(true);
- }}
- />{' '}
- {
- await updateWatchedState({
- id: channel.channel_id,
- is_watched: false,
- });
-
- setRefresh(true);
- }}
- />
-
- >
- )}
-
-
-
-
-
-
- {showEmbeddedVideo && }
-
-
- {!hasVideos && (
- <>
-
No videos found...
-
- Try going to the downloads page to start the scan
- and download tasks.
-
- >
- )}
-
-
-
-
- {pagination && (
-
- )}
- >
- );
-};
-
-export default ChannelVideo;
+import { useEffect, useState } from 'react';
+import {
+ Link,
+ useLoaderData,
+ useOutletContext,
+ useParams,
+ useSearchParams,
+} from 'react-router-dom';
+import { SortByType, SortOrderType, ViewLayoutType } from './Home';
+import { OutletContextType } from './Base';
+import { UserMeType } from '../api/actions/updateUserConfig';
+import VideoList from '../components/VideoList';
+import Routes from '../configuration/routes/RouteList';
+import Pagination from '../components/Pagination';
+import Filterbar from '../components/Filterbar';
+import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
+import ChannelOverview from '../components/ChannelOverview';
+import loadChannelById from '../api/loader/loadChannelById';
+import { ChannelResponseType } from './ChannelBase';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import Button from '../components/Button';
+import loadVideoListByFilter, {
+ VideoListByFilterResponseType,
+ VideoTypes,
+} from '../api/loader/loadVideoListByPage';
+import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
+import humanFileSize from '../functions/humanFileSize';
+
+type ChannelParams = {
+ channelId: string;
+};
+
+type ChannelVideoLoaderType = {
+ userConfig: UserMeType;
+};
+
+type ChannelVideoProps = {
+ videoType: VideoTypes;
+};
+
+const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
+ const { channelId } = useParams() as ChannelParams;
+ const { userConfig } = useLoaderData() as ChannelVideoLoaderType;
+ const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+
+ const userMeConfig = userConfig.config;
+
+ const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
+ const [sortBy, setSortBy] = useState(userMeConfig.sort_by || 'published');
+ const [sortOrder, setSortOrder] = useState(userMeConfig.sort_order || 'asc');
+ const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
+ const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
+ const [refresh, setRefresh] = useState(false);
+
+ const [channelResponse, setChannelResponse] = useState();
+ const [videoResponse, setVideoReponse] = useState();
+ const [videoAggsResponse, setVideoAggsResponse] = useState();
+
+ const channel = channelResponse?.data;
+ const videoList = videoResponse?.data;
+ const pagination = videoResponse?.paginate;
+
+ const hasVideos = videoResponse?.data?.length !== 0;
+ const showEmbeddedVideo = videoId !== null;
+
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refresh ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const channelResponse = await loadChannelById(channelId);
+ const videos = await loadVideoListByFilter({
+ channel: channelId,
+ page: currentPage,
+ watch: hideWatched ? 'unwatched' : undefined,
+ sort: sortBy,
+ order: sortOrder,
+ type: videoType,
+ });
+ const channelAggs = await loadChannelAggs(channelId);
+
+ setChannelResponse(channelResponse);
+ setVideoReponse(videos);
+ setVideoAggsResponse(channelAggs);
+ setRefresh(false);
+ }
+ })();
+ // Do not add sort, order, hideWatched this will not work as expected!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [refresh, currentPage, channelId, pagination?.current_page]);
+
+ if (!channel) {
+ return (
+
+
+
Channel {channelId} not found!
+
+ );
+ }
+
+ return (
+ <>
+ {`TA | Channel: ${channel.channel_name}`}
+
+
+
+
+
+ {videoAggsResponse && (
+ <>
+
+ {videoAggsResponse.total_items.value} videos{' '}
+ | {' '}
+ {videoAggsResponse.total_duration.value_str} playback{' '}
+ | Total size{' '}
+ {humanFileSize(videoAggsResponse.total_size.value, true)}
+
+
+ {
+ await updateWatchedState({
+ id: channel.channel_id,
+ is_watched: true,
+ });
+
+ setRefresh(true);
+ }}
+ />{' '}
+ {
+ await updateWatchedState({
+ id: channel.channel_id,
+ is_watched: false,
+ });
+
+ setRefresh(true);
+ }}
+ />
+
+ >
+ )}
+
+
+
+
+
+
+ {showEmbeddedVideo && }
+
+
+ {!hasVideos && (
+ <>
+
No videos found...
+
+ Try going to the downloads page to start the scan
+ and download tasks.
+
+ >
+ )}
+
+
+
+
+ {pagination && (
+
+ )}
+ >
+ );
+};
+
+export default ChannelVideo;
diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx
index 6d5c7380..c2be6b52 100644
--- a/frontend/src/pages/Channels.tsx
+++ b/frontend/src/pages/Channels.tsx
@@ -1,206 +1,203 @@
-import { useLoaderData, useOutletContext } from 'react-router-dom';
-import loadChannelList from '../api/loader/loadChannelList';
-import iconGridView from '/img/icon-gridview.svg';
-import iconListView from '/img/icon-listview.svg';
-import iconAdd from '/img/icon-add.svg';
-import { useEffect, useState } from 'react';
-import Pagination, { PaginationType } from '../components/Pagination';
-import { ConfigType, ViewLayoutType } from './Home';
-import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
-import { OutletContextType } from './Base';
-import ChannelList from '../components/ChannelList';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import Notifications from '../components/Notifications';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-
-type ChannelOverwritesType = {
- download_format?: string;
- autodelete_days?: number;
- index_playlists?: boolean;
- integrate_sponsorblock?: boolean;
- subscriptions_channel_size?: number;
- subscriptions_live_channel_size?: number;
- subscriptions_shorts_channel_size?: number;
-};
-
-export type ChannelType = {
- channel_active: boolean;
- channel_banner_url: string;
- channel_description: string;
- channel_id: string;
- channel_last_refresh: string;
- channel_name: string;
- channel_overwrites?: ChannelOverwritesType;
- channel_subs: number;
- channel_subscribed: boolean;
- channel_tags: string[];
- channel_thumb_url: string;
- channel_tvart_url: string;
- channel_views: number;
-};
-
-type ChannelsListResponse = {
- data: ChannelType[];
- paginate: PaginationType;
- config?: ConfigType;
-};
-
-type ChannelsLoaderDataType = {
- userConfig: UserMeType;
-};
-
-const Channels = () => {
- const { userConfig } = useLoaderData() as ChannelsLoaderDataType;
- const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
-
- const userMeConfig = userConfig.config;
-
- const [channelListResponse, setChannelListResponse] = useState();
- const [showSubscribedOnly, setShowSubscribedOnly] = useState(
- userMeConfig.show_subed_only || false,
- );
- const [view, setView] = useState(userMeConfig.view_style_channel || 'grid');
- const [showAddForm, setShowAddForm] = useState(false);
- const [refresh, setRefresh] = useState(false);
-
- const channels = channelListResponse?.data;
- const pagination = channelListResponse?.paginate;
- const channelCount = pagination?.total_hits;
- const hasChannels = channels?.length !== 0;
-
- useEffect(() => {
- (async () => {
- if (
- userMeConfig.view_style_channel !== view ||
- userMeConfig.show_subed_only !== showSubscribedOnly
- ) {
- const userConfig: UserConfigType = {
- show_subed_only: showSubscribedOnly,
- view_style_channel: view,
- };
-
- await updateUserConfig(userConfig);
- }
- })();
- }, [showSubscribedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_channel, view]);
-
- useEffect(() => {
- (async () => {
- if (
- refresh ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const channelListResponse = await loadChannelList(currentPage, showSubscribedOnly);
-
- setChannelListResponse(channelListResponse);
- setRefresh(false);
- }
- })();
- }, [currentPage, showSubscribedOnly, refresh, pagination?.current_page]);
-
- return (
- <>
-
- TA | Channels
-
-
-
-
-
-
Channels
-
- {isAdmin && (
-
-
{
- setShowAddForm(!showAddForm);
- }}
- src={iconAdd}
- alt="add-icon"
- title="Subscribe to Channels"
- />
- {showAddForm && (
-
-
- Subscribe to channels:
-
-
-
-
-
- )}
-
- )}
-
-
-
-
-
-
-
Show subscribed only:
-
- {
- setShowSubscribedOnly(!showSubscribedOnly);
- }}
- type="checkbox"
- checked={showSubscribedOnly}
- />
- {!showSubscribedOnly && (
-
- Off
-
- )}
- {showSubscribedOnly && (
-
- On
-
- )}
-
-
-
-
{
- setView('grid');
- }}
- data-origin="channel"
- data-value="grid"
- alt="grid view"
- />
-
{
- setView('list');
- }}
- data-origin="channel"
- data-value="list"
- alt="list view"
- />
-
-
- {hasChannels &&
Total channels: {channelCount} }
-
-
- {!hasChannels &&
No channels found... }
-
- {hasChannels && (
-
- )}
-
-
- {pagination && (
-
- )}
-
- >
- );
-};
-
-export default Channels;
+import { useLoaderData, useOutletContext } from 'react-router-dom';
+import loadChannelList from '../api/loader/loadChannelList';
+import iconGridView from '/img/icon-gridview.svg';
+import iconListView from '/img/icon-listview.svg';
+import iconAdd from '/img/icon-add.svg';
+import { useEffect, useState } from 'react';
+import Pagination, { PaginationType } from '../components/Pagination';
+import { ConfigType, ViewLayoutType } from './Home';
+import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
+import { OutletContextType } from './Base';
+import ChannelList from '../components/ChannelList';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import Notifications from '../components/Notifications';
+import Button from '../components/Button';
+
+type ChannelOverwritesType = {
+ download_format?: string;
+ autodelete_days?: number;
+ index_playlists?: boolean;
+ integrate_sponsorblock?: boolean;
+ subscriptions_channel_size?: number;
+ subscriptions_live_channel_size?: number;
+ subscriptions_shorts_channel_size?: number;
+};
+
+export type ChannelType = {
+ channel_active: boolean;
+ channel_banner_url: string;
+ channel_description: string;
+ channel_id: string;
+ channel_last_refresh: string;
+ channel_name: string;
+ channel_overwrites?: ChannelOverwritesType;
+ channel_subs: number;
+ channel_subscribed: boolean;
+ channel_tags: string[];
+ channel_thumb_url: string;
+ channel_tvart_url: string;
+ channel_views: number;
+};
+
+type ChannelsListResponse = {
+ data: ChannelType[];
+ paginate: PaginationType;
+ config?: ConfigType;
+};
+
+type ChannelsLoaderDataType = {
+ userConfig: UserMeType;
+};
+
+const Channels = () => {
+ const { userConfig } = useLoaderData() as ChannelsLoaderDataType;
+ const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+
+ const userMeConfig = userConfig.config;
+
+ const [channelListResponse, setChannelListResponse] = useState();
+ const [showSubscribedOnly, setShowSubscribedOnly] = useState(
+ userMeConfig.show_subed_only || false,
+ );
+ const [view, setView] = useState(userMeConfig.view_style_channel || 'grid');
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [refresh, setRefresh] = useState(false);
+
+ const channels = channelListResponse?.data;
+ const pagination = channelListResponse?.paginate;
+ const channelCount = pagination?.total_hits;
+ const hasChannels = channels?.length !== 0;
+
+ useEffect(() => {
+ (async () => {
+ if (
+ userMeConfig.view_style_channel !== view ||
+ userMeConfig.show_subed_only !== showSubscribedOnly
+ ) {
+ const userConfig: UserConfigType = {
+ show_subed_only: showSubscribedOnly,
+ view_style_channel: view,
+ };
+
+ await updateUserConfig(userConfig);
+ }
+ })();
+ }, [showSubscribedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_channel, view]);
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refresh ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const channelListResponse = await loadChannelList(currentPage, showSubscribedOnly);
+
+ setChannelListResponse(channelListResponse);
+ setRefresh(false);
+ }
+ })();
+ }, [currentPage, showSubscribedOnly, refresh, pagination?.current_page]);
+
+ return (
+ <>
+ TA | Channels
+
+
+
+
+
Channels
+
+ {isAdmin && (
+
+
{
+ setShowAddForm(!showAddForm);
+ }}
+ src={iconAdd}
+ alt="add-icon"
+ title="Subscribe to Channels"
+ />
+ {showAddForm && (
+
+
+ Subscribe to channels:
+
+
+
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+
Show subscribed only:
+
+ {
+ setShowSubscribedOnly(!showSubscribedOnly);
+ }}
+ type="checkbox"
+ checked={showSubscribedOnly}
+ />
+ {!showSubscribedOnly && (
+
+ Off
+
+ )}
+ {showSubscribedOnly && (
+
+ On
+
+ )}
+
+
+
+
{
+ setView('grid');
+ }}
+ data-origin="channel"
+ data-value="grid"
+ alt="grid view"
+ />
+
{
+ setView('list');
+ }}
+ data-origin="channel"
+ data-value="list"
+ alt="list view"
+ />
+
+
+ {hasChannels &&
Total channels: {channelCount} }
+
+
+ {!hasChannels &&
No channels found... }
+
+ {hasChannels && (
+
+ )}
+
+
+ {pagination && (
+
+ )}
+
+ >
+ );
+};
+
+export default Channels;
diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx
index 02564e3c..e12ddbe7 100644
--- a/frontend/src/pages/Download.tsx
+++ b/frontend/src/pages/Download.tsx
@@ -16,7 +16,6 @@ import updateDownloadQueue from '../api/actions/updateDownloadQueue';
import updateTaskByName from '../api/actions/updateTaskByName';
import Notifications from '../components/Notifications';
import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import { Helmet } from 'react-helmet';
import Button from '../components/Button';
import DownloadListItem from '../components/DownloadListItem';
import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs';
@@ -90,20 +89,14 @@ const Download = () => {
useEffect(() => {
(async () => {
- if (
- userMeConfig.show_ignored_only !== showIgnored ||
- userMeConfig.view_style_downloads !== view ||
- userMeConfig.grid_items !== gridItems
- ) {
- const userConfig: UserConfigType = {
- show_ignored_only: showIgnored,
- [ViewStyleNames.downloads]: view,
- grid_items: gridItems,
- };
+ const userConfig: UserConfigType = {
+ show_ignored_only: showIgnored,
+ [ViewStyleNames.downloads]: view,
+ grid_items: gridItems,
+ };
- await updateUserConfig(userConfig);
- setRefresh(true);
- }
+ await updateUserConfig(userConfig);
+ setRefresh(true);
})();
}, [
view,
@@ -152,9 +145,7 @@ const Download = () => {
return (
<>
-
- TA | Downloads
-
+ TA | Downloads
diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx
index 75ae2bb2..b32dff97 100644
--- a/frontend/src/pages/ErrorPage.tsx
+++ b/frontend/src/pages/ErrorPage.tsx
@@ -1,35 +1,32 @@
-import { Helmet } from 'react-helmet';
-import { useRouteError } from 'react-router-dom';
-import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
-
-// This is not always the correct response
-type ErrorType = {
- statusText: string;
- message: string;
-};
-
-const ErrorPage = () => {
- const error = useRouteError() as ErrorType;
- importColours(ColourConstant.Dark as ColourVariants);
-
- console.error('ErrorPage', error);
-
- return (
- <>
-
- TA | Oops!
-
-
-
-
Oops!
-
Sorry, an unexpected error has occurred.
-
- {error?.statusText}
- {error?.message}
-
-
- >
- );
-};
-
-export default ErrorPage;
+import { useRouteError } from 'react-router-dom';
+import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
+
+// This is not always the correct response
+type ErrorType = {
+ statusText: string;
+ message: string;
+};
+
+const ErrorPage = () => {
+ const error = useRouteError() as ErrorType;
+ importColours(ColourConstant.Dark as ColourVariants);
+
+ console.error('ErrorPage', error);
+
+ return (
+ <>
+
TA | Oops!
+
+
+
Oops!
+
Sorry, an unexpected error has occurred.
+
+ {error?.statusText}
+ {error?.message}
+
+
+ >
+ );
+};
+
+export default ErrorPage;
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 5e4fa497..79d7d718 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -1,251 +1,248 @@
-import { useEffect, useState } from 'react';
-import { Link, useLoaderData, useOutletContext, useSearchParams } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import Pagination from '../components/Pagination';
-import loadVideoListByFilter, {
- VideoListByFilterResponseType,
-} from '../api/loader/loadVideoListByPage';
-import { UserMeType } from '../api/actions/updateUserConfig';
-import VideoList from '../components/VideoList';
-import { ChannelType } from './Channels';
-import { OutletContextType } from './Base';
-import Filterbar from '../components/Filterbar';
-import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import { Helmet } from 'react-helmet';
-import { SponsorBlockType } from './Video';
-
-export type PlayerType = {
- watched: boolean;
- duration: number;
- duration_str: string;
- progress: number;
-};
-
-export type StatsType = {
- view_count: number;
- like_count: number;
- dislike_count: number;
- average_rating: number;
-};
-
-export type StreamType = {
- type: string;
- index: number;
- codec: string;
- width?: number;
- height?: number;
- bitrate: number;
-};
-
-export type Subtitles = {
- ext: string;
- url: string;
- name: string;
- lang: string;
- source: string;
- media_url: string;
-};
-
-export type VideoType = {
- active: boolean;
- category: string[];
- channel: ChannelType;
- date_downloaded: number;
- description: string;
- comment_count?: number;
- media_size: number;
- media_url: string;
- player: PlayerType;
- published: string;
- sponsorblock?: SponsorBlockType;
- playlist?: string[];
- stats: StatsType;
- streams: StreamType[];
- subtitles: Subtitles[];
- tags: string[];
- title: string;
- vid_last_refresh: string;
- vid_thumb_base64: boolean;
- vid_thumb_url: string;
- vid_type: string;
- youtube_id: string;
-};
-
-export type DownloadsType = {
- limit_speed: boolean;
- sleep_interval: number;
- autodelete_days: boolean;
- format: boolean;
- format_sort: boolean;
- add_metadata: boolean;
- add_thumbnail: boolean;
- subtitle: boolean;
- subtitle_source: boolean;
- subtitle_index: boolean;
- comment_max: boolean;
- comment_sort: string;
- cookie_import: boolean;
- throttledratelimit: boolean;
- extractor_lang: boolean;
- integrate_ryd: boolean;
- integrate_sponsorblock: boolean;
-};
-
-export type ConfigType = {
- enable_cast: boolean;
- downloads: DownloadsType;
-};
-
-type HomeLoaderDataType = {
- userConfig: UserMeType;
-};
-
-export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'filesize';
-export type SortOrderType = 'asc' | 'desc';
-export type ViewLayoutType = 'grid' | 'list';
-
-const Home = () => {
- const { userConfig } = useLoaderData() as HomeLoaderDataType;
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
-
- const userMeConfig = userConfig.config;
-
- const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
- const [sortBy, setSortBy] = useState
(userMeConfig.sort_by || 'published');
- const [sortOrder, setSortOrder] = useState(userMeConfig.sort_order || 'asc');
- const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
- const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
- const [showHidden, setShowHidden] = useState(false);
- const [refreshVideoList, setRefreshVideoList] = useState(false);
-
- const [videoResponse, setVideoReponse] = useState();
- const [continueVideoResponse, setContinueVideoResponse] =
- useState();
-
- const videoList = videoResponse?.data;
- const pagination = videoResponse?.paginate;
- const continueVideos = continueVideoResponse?.data;
-
- const hasVideos = videoResponse?.data?.length !== 0;
- const showEmbeddedVideo = videoId !== null;
-
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- if (
- refreshVideoList ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const videos = await loadVideoListByFilter({
- page: currentPage,
- watch: hideWatched ? 'unwatched' : undefined,
- sort: sortBy,
- order: sortOrder,
- });
-
- try {
- const continueVideoResponse = await loadVideoListByFilter({ watch: 'continue' });
- setContinueVideoResponse(continueVideoResponse);
- } catch (error) {
- console.log('Server error on continue vids?');
- }
-
- setVideoReponse(videos);
-
- setRefreshVideoList(false);
- }
- })();
- // Do not add sort, order, hideWatched this will not work as expected!
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [refreshVideoList, currentPage, pagination?.current_page]);
-
- return (
- <>
-
- TubeArchivist
-
-
-
- {continueVideos && continueVideos.length > 0 && (
- <>
-
-
Continue Watching
-
-
-
-
- >
- )}
-
-
-
Recent Videos
-
-
-
-
-
- {showEmbeddedVideo && }
-
-
-
- {!hasVideos && (
- <>
-
No videos found...
-
- If you've already added a channel or playlist, try going to the{' '}
- downloads page to start the scan and download
- tasks.
-
- >
- )}
-
- {hasVideos && (
-
- )}
-
-
-
- {pagination && (
-
- )}
- >
- );
-};
-
-export default Home;
+import { useEffect, useState } from 'react';
+import { Link, useLoaderData, useOutletContext, useSearchParams } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import Pagination from '../components/Pagination';
+import loadVideoListByFilter, {
+ VideoListByFilterResponseType,
+} from '../api/loader/loadVideoListByPage';
+import { UserMeType } from '../api/actions/updateUserConfig';
+import VideoList from '../components/VideoList';
+import { ChannelType } from './Channels';
+import { OutletContextType } from './Base';
+import Filterbar from '../components/Filterbar';
+import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import { SponsorBlockType } from './Video';
+
+export type PlayerType = {
+ watched: boolean;
+ duration: number;
+ duration_str: string;
+ progress: number;
+};
+
+export type StatsType = {
+ view_count: number;
+ like_count: number;
+ dislike_count: number;
+ average_rating: number;
+};
+
+export type StreamType = {
+ type: string;
+ index: number;
+ codec: string;
+ width?: number;
+ height?: number;
+ bitrate: number;
+};
+
+export type Subtitles = {
+ ext: string;
+ url: string;
+ name: string;
+ lang: string;
+ source: string;
+ media_url: string;
+};
+
+export type VideoType = {
+ active: boolean;
+ category: string[];
+ channel: ChannelType;
+ date_downloaded: number;
+ description: string;
+ comment_count?: number;
+ media_size: number;
+ media_url: string;
+ player: PlayerType;
+ published: string;
+ sponsorblock?: SponsorBlockType;
+ playlist?: string[];
+ stats: StatsType;
+ streams: StreamType[];
+ subtitles: Subtitles[];
+ tags: string[];
+ title: string;
+ vid_last_refresh: string;
+ vid_thumb_base64: boolean;
+ vid_thumb_url: string;
+ vid_type: string;
+ youtube_id: string;
+};
+
+export type DownloadsType = {
+ limit_speed: boolean;
+ sleep_interval: number;
+ autodelete_days: boolean;
+ format: boolean;
+ format_sort: boolean;
+ add_metadata: boolean;
+ add_thumbnail: boolean;
+ subtitle: boolean;
+ subtitle_source: boolean;
+ subtitle_index: boolean;
+ comment_max: boolean;
+ comment_sort: string;
+ cookie_import: boolean;
+ throttledratelimit: boolean;
+ extractor_lang: boolean;
+ integrate_ryd: boolean;
+ integrate_sponsorblock: boolean;
+};
+
+export type ConfigType = {
+ enable_cast: boolean;
+ downloads: DownloadsType;
+};
+
+type HomeLoaderDataType = {
+ userConfig: UserMeType;
+};
+
+export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'filesize';
+export type SortOrderType = 'asc' | 'desc';
+export type ViewLayoutType = 'grid' | 'list';
+
+const Home = () => {
+ const { userConfig } = useLoaderData() as HomeLoaderDataType;
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+
+ const userMeConfig = userConfig.config;
+
+ const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
+ const [sortBy, setSortBy] = useState(userMeConfig.sort_by || 'published');
+ const [sortOrder, setSortOrder] = useState(userMeConfig.sort_order || 'asc');
+ const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
+ const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
+ const [showHidden, setShowHidden] = useState(false);
+ const [refreshVideoList, setRefreshVideoList] = useState(false);
+
+ const [videoResponse, setVideoReponse] = useState();
+ const [continueVideoResponse, setContinueVideoResponse] =
+ useState();
+
+ const videoList = videoResponse?.data;
+ const pagination = videoResponse?.paginate;
+ const continueVideos = continueVideoResponse?.data;
+
+ const hasVideos = videoResponse?.data?.length !== 0;
+ const showEmbeddedVideo = videoId !== null;
+
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refreshVideoList ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const videos = await loadVideoListByFilter({
+ page: currentPage,
+ watch: hideWatched ? 'unwatched' : undefined,
+ sort: sortBy,
+ order: sortOrder,
+ });
+
+ try {
+ const continueVideoResponse = await loadVideoListByFilter({ watch: 'continue' });
+ setContinueVideoResponse(continueVideoResponse);
+ } catch (error) {
+ console.log('Server error on continue vids?');
+ }
+
+ setVideoReponse(videos);
+
+ setRefreshVideoList(false);
+ }
+ })();
+ // Do not add sort, order, hideWatched this will not work as expected!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [refreshVideoList, currentPage, pagination?.current_page]);
+
+ return (
+ <>
+ TubeArchivist
+
+
+ {continueVideos && continueVideos.length > 0 && (
+ <>
+
+
Continue Watching
+
+
+
+
+ >
+ )}
+
+
+
Recent Videos
+
+
+
+
+
+ {showEmbeddedVideo && }
+
+
+
+ {!hasVideos && (
+ <>
+
No videos found...
+
+ If you've already added a channel or playlist, try going to the{' '}
+ downloads page to start the scan and download
+ tasks.
+
+ >
+ )}
+
+ {hasVideos && (
+
+ )}
+
+
+
+ {pagination && (
+
+ )}
+ >
+ );
+};
+
+export default Home;
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index 4448976f..c8ac4729 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -1,106 +1,103 @@
-import { useState } from 'react';
-import Routes from '../configuration/routes/RouteList';
-import { useNavigate } from 'react-router-dom';
-import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-import signIn from '../api/actions/signIn';
-
-const Login = () => {
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [saveLogin, setSaveLogin] = useState(false);
- const navigate = useNavigate();
-
- importColours(ColourConstant.Dark as ColourVariants);
-
- const form_error = false;
-
- const handleSubmit = async (event: { preventDefault: () => void }) => {
- event.preventDefault();
-
- const loginResponse = await signIn(username, password, saveLogin);
-
- const signedIn = loginResponse.status === 200;
-
- if (signedIn) {
- navigate(Routes.Home);
- } else {
- navigate(Routes.Login);
- }
- };
-
- return (
- <>
-
- TA | Welcome
-
-
-
-
Tube Archivist
-
Your Self Hosted YouTube Media Server
-
- {form_error &&
Failed to login.
}
-
-
-
-
-
- Github
-
- {' '}
-
-
- Donate
-
-
-
-
-
- >
- );
-};
-
-export default Login;
+import { useState } from 'react';
+import Routes from '../configuration/routes/RouteList';
+import { useNavigate } from 'react-router-dom';
+import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
+import Button from '../components/Button';
+import signIn from '../api/actions/signIn';
+
+const Login = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [saveLogin, setSaveLogin] = useState(false);
+ const navigate = useNavigate();
+
+ importColours(ColourConstant.Dark as ColourVariants);
+
+ const form_error = false;
+
+ const handleSubmit = async (event: { preventDefault: () => void }) => {
+ event.preventDefault();
+
+ const loginResponse = await signIn(username, password, saveLogin);
+
+ const signedIn = loginResponse.status === 200;
+
+ if (signedIn) {
+ navigate(Routes.Home);
+ } else {
+ navigate(Routes.Login);
+ }
+ };
+
+ return (
+ <>
+ TA | Welcome
+
+
+
Tube Archivist
+
Your Self Hosted YouTube Media Server
+
+ {form_error &&
Failed to login.
}
+
+
+
+
+
+ Github
+
+ {' '}
+
+
+ Donate
+
+
+
+
+
+ >
+ );
+};
+
+export default Login;
diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx
index c554474d..c8148009 100644
--- a/frontend/src/pages/Playlist.tsx
+++ b/frontend/src/pages/Playlist.tsx
@@ -1,390 +1,388 @@
-import { useEffect, useState } from 'react';
-import {
- Link,
- useLoaderData,
- useNavigate,
- useOutletContext,
- useParams,
- useSearchParams,
-} from 'react-router-dom';
-import { UserMeType } from '../api/actions/updateUserConfig';
-import loadPlaylistById from '../api/loader/loadPlaylistById';
-import { OutletContextType } from './Base';
-import { ConfigType, VideoType, ViewLayoutType } from './Home';
-import Filterbar from '../components/Filterbar';
-import { PlaylistEntryType } from './Playlists';
-import loadChannelById from '../api/loader/loadChannelById';
-import VideoList from '../components/VideoList';
-import Pagination, { PaginationType } from '../components/Pagination';
-import ChannelOverview from '../components/ChannelOverview';
-import Linkify from '../components/Linkify';
-import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
-import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
-import deletePlaylist from '../api/actions/deletePlaylist';
-import Routes from '../configuration/routes/RouteList';
-import { ChannelResponseType } from './ChannelBase';
-import formatDate from '../functions/formatDates';
-import queueReindex from '../api/actions/queueReindex';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
-
-export type PlaylistType = {
- playlist_active: boolean;
- playlist_channel: string;
- playlist_channel_id: string;
- playlist_description: string;
- playlist_entries: PlaylistEntryType[];
- playlist_id: string;
- playlist_last_refresh: string;
- playlist_name: string;
- playlist_subscribed: boolean;
- playlist_thumbnail: string;
- playlist_type: string;
- _index: string;
- _score: number;
-};
-
-type PlaylistLoaderDataType = {
- userConfig: UserMeType;
-};
-
-export type PlaylistResponseType = {
- data?: PlaylistType;
- config?: ConfigType;
-};
-
-export type VideoResponseType = {
- data?: VideoType[];
- config?: ConfigType;
- paginate?: PaginationType;
-};
-
-const Playlist = () => {
- const { playlistId } = useParams();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
-
- const { userConfig } = useLoaderData() as PlaylistLoaderDataType;
- const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
-
- const userMeConfig = userConfig.config;
-
- const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
- const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
- const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
- const [refresh, setRefresh] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [reindex, setReindex] = useState(false);
-
- const [playlistResponse, setPlaylistResponse] = useState();
- const [channelResponse, setChannelResponse] = useState();
- const [videoResponse, setVideoResponse] = useState();
-
- const playlist = playlistResponse?.data;
- const channel = channelResponse?.data;
- const videos = videoResponse?.data;
- const pagination = videoResponse?.paginate;
-
- const palylistEntries = playlistResponse?.data?.playlist_entries;
- const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
- const videoInPlaylistCount = pagination?.total_hits;
- const showEmbeddedVideo = videoId !== null;
-
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- if (
- refresh ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const playlist = await loadPlaylistById(playlistId);
- const video = await loadVideoListByFilter({
- playlist: playlistId,
- page: currentPage,
- watch: hideWatched ? 'unwatched' : undefined,
- sort: 'downloaded', // downloaded or published? or playlist sort order?
- });
-
- const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
- if (!isCustomPlaylist) {
- const channel = await loadChannelById(playlist.data.playlist_channel_id);
-
- setChannelResponse(channel);
- }
-
- setPlaylistResponse(playlist);
- setVideoResponse(video);
- setRefresh(false);
- }
- })();
- // Do not add hideWatched this will not work as expected!
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [playlistId, refresh, currentPage, pagination?.current_page]);
-
- if (!playlistId || !playlist) {
- return `Playlist ${playlistId} not found!`;
- }
-
- const isCustomPlaylist = playlist.playlist_type === 'custom';
-
- return (
- <>
-
- TA | Playlist: {playlist.playlist_name}
-
-
-
-
-
{playlist.playlist_name}
-
-
- {!isCustomPlaylist && channel && (
-
- )}
-
-
-
-
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
- {!isCustomPlaylist && (
- <>
-
- Playlist:
- {playlist.playlist_subscribed && (
- <>
- {isAdmin && (
- {
- await updatePlaylistSubscription(playlistId, false);
-
- setRefresh(true);
- }}
- />
- )}
- >
- )}{' '}
- {!playlist.playlist_subscribed && (
- {
- await updatePlaylistSubscription(playlistId, true);
-
- setRefresh(true);
- }}
- />
- )}
-
- {playlist.playlist_active && (
-
- Youtube:{' '}
-
- Active
-
-
- )}
- {!playlist.playlist_active &&
Youtube: Deactivated
}
- >
- )}
-
- {!showDeleteConfirm && (
-
setShowDeleteConfirm(!showDeleteConfirm)}
- />
- )}
-
- {showDeleteConfirm && (
-
- Delete {playlist.playlist_name}?
-
- {
- await deletePlaylist(playlistId, false);
- navigate(Routes.Playlists);
- }}
- />
-
- {
- await deletePlaylist(playlistId, true);
- navigate(Routes.Playlists);
- }}
- />
-
-
- setShowDeleteConfirm(!showDeleteConfirm)} />
-
- )}
-
-
-
-
- {videoArchivedCount > 0 && (
- <>
-
- Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
-
-
- {
- await updateWatchedState({
- id: playlistId,
- is_watched: true,
- });
-
- setRefresh(true);
- }}
- />{' '}
- {
- await updateWatchedState({
- id: playlistId,
- is_watched: false,
- });
-
- setRefresh(true);
- }}
- />
-
- >
- )}
-
- {reindex &&
Reindex scheduled
}
- {!reindex && (
-
- {!isCustomPlaylist && (
- {
- setReindex(true);
-
- await queueReindex(playlist.playlist_id, 'playlist');
- }}
- />
- )}{' '}
- {
- setReindex(true);
-
- await queueReindex(playlist.playlist_id, 'playlist', true);
- }}
- />
-
- )}
-
-
-
-
- {playlist.playlist_description && (
-
-
- {playlist.playlist_description}
-
-
-
setDescriptionExpanded(!descriptionExpanded)}
- />
-
- )}
-
-
-
-
-
-
- {showEmbeddedVideo && }
-
-
-
- {videoInPlaylistCount === 0 && (
- <>
-
No videos found...
- {isCustomPlaylist && (
-
- Try going to the home page to add videos to this
- playlist.
-
- )}
-
- {!isCustomPlaylist && (
-
- Try going to the downloads page to start the
- scan and download tasks.
-
- )}
- >
- )}
- {videoInPlaylistCount !== 0 && (
-
- )}
-
-
-
-
- >
- );
-};
-
-export default Playlist;
+import { useEffect, useState } from 'react';
+import {
+ Link,
+ useLoaderData,
+ useNavigate,
+ useOutletContext,
+ useParams,
+ useSearchParams,
+} from 'react-router-dom';
+import { UserMeType } from '../api/actions/updateUserConfig';
+import loadPlaylistById from '../api/loader/loadPlaylistById';
+import { OutletContextType } from './Base';
+import { ConfigType, VideoType, ViewLayoutType } from './Home';
+import Filterbar from '../components/Filterbar';
+import { PlaylistEntryType } from './Playlists';
+import loadChannelById from '../api/loader/loadChannelById';
+import VideoList from '../components/VideoList';
+import Pagination, { PaginationType } from '../components/Pagination';
+import ChannelOverview from '../components/ChannelOverview';
+import Linkify from '../components/Linkify';
+import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
+import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
+import deletePlaylist from '../api/actions/deletePlaylist';
+import Routes from '../configuration/routes/RouteList';
+import { ChannelResponseType } from './ChannelBase';
+import formatDate from '../functions/formatDates';
+import queueReindex from '../api/actions/queueReindex';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import Button from '../components/Button';
+import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
+
+export type PlaylistType = {
+ playlist_active: boolean;
+ playlist_channel: string;
+ playlist_channel_id: string;
+ playlist_description: string;
+ playlist_entries: PlaylistEntryType[];
+ playlist_id: string;
+ playlist_last_refresh: string;
+ playlist_name: string;
+ playlist_subscribed: boolean;
+ playlist_thumbnail: string;
+ playlist_type: string;
+ _index: string;
+ _score: number;
+};
+
+type PlaylistLoaderDataType = {
+ userConfig: UserMeType;
+};
+
+export type PlaylistResponseType = {
+ data?: PlaylistType;
+ config?: ConfigType;
+};
+
+export type VideoResponseType = {
+ data?: VideoType[];
+ config?: ConfigType;
+ paginate?: PaginationType;
+};
+
+const Playlist = () => {
+ const { playlistId } = useParams();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+
+ const { userConfig } = useLoaderData() as PlaylistLoaderDataType;
+ const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+
+ const userMeConfig = userConfig.config;
+
+ const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
+ const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
+ const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
+ const [descriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [refresh, setRefresh] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [reindex, setReindex] = useState(false);
+
+ const [playlistResponse, setPlaylistResponse] = useState();
+ const [channelResponse, setChannelResponse] = useState();
+ const [videoResponse, setVideoResponse] = useState();
+
+ const playlist = playlistResponse?.data;
+ const channel = channelResponse?.data;
+ const videos = videoResponse?.data;
+ const pagination = videoResponse?.paginate;
+
+ const palylistEntries = playlistResponse?.data?.playlist_entries;
+ const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
+ const videoInPlaylistCount = pagination?.total_hits;
+ const showEmbeddedVideo = videoId !== null;
+
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refresh ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const playlist = await loadPlaylistById(playlistId);
+ const video = await loadVideoListByFilter({
+ playlist: playlistId,
+ page: currentPage,
+ watch: hideWatched ? 'unwatched' : undefined,
+ sort: 'downloaded', // downloaded or published? or playlist sort order?
+ });
+
+ const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
+ if (!isCustomPlaylist) {
+ const channel = await loadChannelById(playlist.data.playlist_channel_id);
+
+ setChannelResponse(channel);
+ }
+
+ setPlaylistResponse(playlist);
+ setVideoResponse(video);
+ setRefresh(false);
+ }
+ })();
+ // Do not add hideWatched this will not work as expected!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [playlistId, refresh, currentPage, pagination?.current_page]);
+
+ if (!playlistId || !playlist) {
+ return `Playlist ${playlistId} not found!`;
+ }
+
+ const isCustomPlaylist = playlist.playlist_type === 'custom';
+
+ return (
+ <>
+ {`TA | Playlist: ${playlist.playlist_name}`}
+
+
+
+
{playlist.playlist_name}
+
+
+ {!isCustomPlaylist && channel && (
+
+ )}
+
+
+
+
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
+ {!isCustomPlaylist && (
+ <>
+
+ Playlist:
+ {playlist.playlist_subscribed && (
+ <>
+ {isAdmin && (
+ {
+ await updatePlaylistSubscription(playlistId, false);
+
+ setRefresh(true);
+ }}
+ />
+ )}
+ >
+ )}{' '}
+ {!playlist.playlist_subscribed && (
+ {
+ await updatePlaylistSubscription(playlistId, true);
+
+ setRefresh(true);
+ }}
+ />
+ )}
+
+ {playlist.playlist_active && (
+
+ Youtube:{' '}
+
+ Active
+
+
+ )}
+ {!playlist.playlist_active &&
Youtube: Deactivated
}
+ >
+ )}
+
+ {!showDeleteConfirm && (
+
setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+ )}
+
+ {showDeleteConfirm && (
+
+ Delete {playlist.playlist_name}?
+
+ {
+ await deletePlaylist(playlistId, false);
+ navigate(Routes.Playlists);
+ }}
+ />
+
+ {
+ await deletePlaylist(playlistId, true);
+ navigate(Routes.Playlists);
+ }}
+ />
+
+
+ setShowDeleteConfirm(!showDeleteConfirm)} />
+
+ )}
+
+
+
+
+ {videoArchivedCount > 0 && (
+ <>
+
+ Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
+
+
+ {
+ await updateWatchedState({
+ id: playlistId,
+ is_watched: true,
+ });
+
+ setRefresh(true);
+ }}
+ />{' '}
+ {
+ await updateWatchedState({
+ id: playlistId,
+ is_watched: false,
+ });
+
+ setRefresh(true);
+ }}
+ />
+
+ >
+ )}
+
+ {reindex &&
Reindex scheduled
}
+ {!reindex && (
+
+ {!isCustomPlaylist && (
+ {
+ setReindex(true);
+
+ await queueReindex(playlist.playlist_id, 'playlist');
+ }}
+ />
+ )}{' '}
+ {
+ setReindex(true);
+
+ await queueReindex(playlist.playlist_id, 'playlist', true);
+ }}
+ />
+
+ )}
+
+
+
+
+ {playlist.playlist_description && (
+
+
+ {playlist.playlist_description}
+
+
+
setDescriptionExpanded(!descriptionExpanded)}
+ />
+
+ )}
+
+
+
+
+
+
+ {showEmbeddedVideo && }
+
+
+
+ {videoInPlaylistCount === 0 && (
+ <>
+
No videos found...
+ {isCustomPlaylist && (
+
+ Try going to the home page to add videos to this
+ playlist.
+
+ )}
+
+ {!isCustomPlaylist && (
+
+ Try going to the downloads page to start the
+ scan and download tasks.
+
+ )}
+ >
+ )}
+ {videoInPlaylistCount !== 0 && (
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Playlist;
diff --git a/frontend/src/pages/Playlists.tsx b/frontend/src/pages/Playlists.tsx
index 5b995fc4..d4dfbd7c 100644
--- a/frontend/src/pages/Playlists.tsx
+++ b/frontend/src/pages/Playlists.tsx
@@ -1,219 +1,222 @@
-import { useEffect, useState } from 'react';
-import { useLoaderData, useOutletContext } from 'react-router-dom';
-
-import iconAdd from '/img/icon-add.svg';
-import iconGridView from '/img/icon-gridview.svg';
-import iconListView from '/img/icon-listview.svg';
-
-import { OutletContextType } from './Base';
-import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
-import loadPlaylistList from '../api/loader/loadPlaylistList';
-import { ConfigType, ViewLayoutType } from './Home';
-import Pagination, { PaginationType } from '../components/Pagination';
-import PlaylistList from '../components/PlaylistList';
-import { PlaylistType } from './Playlist';
-import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
-import createCustomPlaylist from '../api/actions/createCustomPlaylist';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-
-export type PlaylistEntryType = {
- youtube_id: string;
- title: string;
- uploader: string;
- idx: number;
- downloaded: boolean;
-};
-
-export type PlaylistsResponseType = {
- data?: PlaylistType[];
- config?: ConfigType;
- paginate?: PaginationType;
-};
-
-type PlaylistLoaderDataType = {
- userConfig: UserMeType;
-};
-
-const Playlists = () => {
- const { userConfig } = useLoaderData() as PlaylistLoaderDataType;
- const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
-
- const userMeConfig = userConfig.config;
-
- const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false);
- const [view, setView] = useState(userMeConfig.view_style_playlist || 'grid');
- const [showAddForm, setShowAddForm] = useState(false);
- const [refresh, setRefresh] = useState(false);
- const [playlistsToAddText, setPlaylistsToAddText] = useState('');
- const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
-
- const [playlistResponse, setPlaylistReponse] = useState();
-
- const playlistList = playlistResponse?.data;
- const pagination = playlistResponse?.paginate;
-
- const hasPlaylists = playlistResponse?.data?.length !== 0;
-
- useEffect(() => {
- (async () => {
- if (
- userMeConfig.view_style_playlist !== view ||
- userMeConfig.show_subed_only !== showSubedOnly
- ) {
- const userConfig: UserConfigType = {
- show_subed_only: showSubedOnly,
- view_style_playlist: view,
- };
-
- await updateUserConfig(userConfig);
- }
- })();
- }, [showSubedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_playlist, view]);
-
- useEffect(() => {
- (async () => {
- if (
- refresh ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const playlist = await loadPlaylistList({ page: currentPage });
-
- setPlaylistReponse(playlist);
- setRefresh(false);
- }
- })();
- }, [refresh, currentPage, showSubedOnly, view, pagination?.current_page]);
-
- return (
- <>
-
- TA | Playlists
-
-
-
-
-
-
Playlists
-
- {isAdmin && (
-
-
{
- setShowAddForm(!showAddForm);
- }}
- src={iconAdd}
- alt="add-icon"
- title="Subscribe to Playlists"
- />
- {showAddForm && (
-
-
- Subscribe to playlists:
-
-
-
- Or create custom playlist:
-
-
- )}
-
- )}
-
-
-
-
-
-
-
Show subscribed only:
-
- {
- setShowSubedOnly(!showSubedOnly);
- }}
- type="checkbox"
- />
- {!showSubedOnly && (
-
- Off
-
- )}
- {showSubedOnly && (
-
- On
-
- )}
-
-
-
-
{
- setView('grid');
- }}
- alt="grid view"
- />
-
{
- setView('list');
- }}
- alt="list view"
- />
-
-
-
-
- {!hasPlaylists &&
No playlists found... }
-
- {hasPlaylists && (
-
- )}
-
-
-
-
- >
- );
-};
-
-export default Playlists;
+import { useEffect, useState } from 'react';
+import { useLoaderData, useOutletContext } from 'react-router-dom';
+
+import iconAdd from '/img/icon-add.svg';
+import iconGridView from '/img/icon-gridview.svg';
+import iconListView from '/img/icon-listview.svg';
+
+import { OutletContextType } from './Base';
+import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
+import loadPlaylistList from '../api/loader/loadPlaylistList';
+import { ConfigType, ViewLayoutType } from './Home';
+import Pagination, { PaginationType } from '../components/Pagination';
+import PlaylistList from '../components/PlaylistList';
+import { PlaylistType } from './Playlist';
+import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
+import createCustomPlaylist from '../api/actions/createCustomPlaylist';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import Button from '../components/Button';
+
+export type PlaylistEntryType = {
+ youtube_id: string;
+ title: string;
+ uploader: string;
+ idx: number;
+ downloaded: boolean;
+};
+
+export type PlaylistsResponseType = {
+ data?: PlaylistType[];
+ config?: ConfigType;
+ paginate?: PaginationType;
+};
+
+type PlaylistLoaderDataType = {
+ userConfig: UserMeType;
+};
+
+const Playlists = () => {
+ const { userConfig } = useLoaderData() as PlaylistLoaderDataType;
+ const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+
+ const userMeConfig = userConfig.config;
+
+ const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false);
+ const [view, setView] = useState(userMeConfig.view_style_playlist || 'grid');
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [refresh, setRefresh] = useState(false);
+ const [playlistsToAddText, setPlaylistsToAddText] = useState('');
+ const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
+
+ const [playlistResponse, setPlaylistReponse] = useState();
+
+ const playlistList = playlistResponse?.data;
+ const pagination = playlistResponse?.paginate;
+
+ const hasPlaylists = playlistResponse?.data?.length !== 0;
+
+ useEffect(() => {
+ (async () => {
+ if (
+ userMeConfig.view_style_playlist !== view ||
+ userMeConfig.show_subed_only !== showSubedOnly
+ ) {
+ const userConfig: UserConfigType = {
+ show_subed_only: showSubedOnly,
+ view_style_playlist: view,
+ };
+
+ await updateUserConfig(userConfig);
+ setRefresh(true);
+ }
+ })();
+ }, [showSubedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_playlist, view]);
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refresh ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const playlist = await loadPlaylistList({
+ page: currentPage,
+ subscribed: showSubedOnly,
+ });
+
+ setPlaylistReponse(playlist);
+ setRefresh(false);
+ }
+ })();
+ // Do not add showSubedOnly, view this will not work as expected!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [refresh, currentPage, pagination?.current_page]);
+
+ return (
+ <>
+ TA | Playlists
+
+
+
+
+
Playlists
+
+ {isAdmin && (
+
+
{
+ setShowAddForm(!showAddForm);
+ }}
+ src={iconAdd}
+ alt="add-icon"
+ title="Subscribe to Playlists"
+ />
+ {showAddForm && (
+
+
+ Subscribe to playlists:
+
+
+
+ Or create custom playlist:
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+
Show subscribed only:
+
+ {
+ setShowSubedOnly(!showSubedOnly);
+ }}
+ type="checkbox"
+ />
+ {!showSubedOnly && (
+
+ Off
+
+ )}
+ {showSubedOnly && (
+
+ On
+
+ )}
+
+
+
+
{
+ setView('grid');
+ }}
+ alt="grid view"
+ />
+
{
+ setView('list');
+ }}
+ alt="list view"
+ />
+
+
+
+
+ {!hasPlaylists &&
No playlists found... }
+
+ {hasPlaylists && (
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Playlists;
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx
index 68765792..af4618ab 100644
--- a/frontend/src/pages/Search.tsx
+++ b/frontend/src/pages/Search.tsx
@@ -1,170 +1,167 @@
-import { useLoaderData, useSearchParams } from 'react-router-dom';
-import { UserMeType } from '../api/actions/updateUserConfig';
-import { useEffect, useState } from 'react';
-import { VideoType, ViewLayoutType } from './Home';
-import loadSearch from '../api/loader/loadSearch';
-import { PlaylistType } from './Playlist';
-import { ChannelType } from './Channels';
-import VideoList from '../components/VideoList';
-import ChannelList from '../components/ChannelList';
-import PlaylistList from '../components/PlaylistList';
-import SubtitleList from '../components/SubtitleList';
-import { ViewStyles } from '../configuration/constants/ViewStyle';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import { Helmet } from 'react-helmet';
-import SearchExampleQueries from '../components/SearchExampleQueries';
-
-const EmptySearchResponse: SearchResultsType = {
- results: {
- video_results: [],
- channel_results: [],
- playlist_results: [],
- fulltext_results: [],
- },
- queryType: 'simple',
-};
-
-type SearchResultType = {
- video_results: VideoType[];
- channel_results: ChannelType[];
- playlist_results: PlaylistType[];
- fulltext_results: [];
-};
-
-type SearchResultsType = {
- results: SearchResultType;
- queryType: string;
-};
-
-type SearchLoaderDataType = {
- userConfig: UserMeType;
-};
-
-const Search = () => {
- const { userConfig } = useLoaderData() as SearchLoaderDataType;
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
- const userMeConfig = userConfig.config;
-
- const view = (userMeConfig.view_style_home || ViewStyles.grid) as ViewLayoutType;
- const gridItems = userMeConfig.grid_items || 3;
-
- const [searchQuery, setSearchQuery] = useState('');
- const [searchResults, setSearchResults] = useState();
-
- const [refresh, setRefresh] = useState(false);
-
- const videoList = searchResults?.results.video_results;
- const channelList = searchResults?.results.channel_results;
- const playlistList = searchResults?.results.playlist_results;
- const fulltextList = searchResults?.results.fulltext_results;
- const queryType = searchResults?.queryType;
- const showEmbeddedVideo = videoId !== null;
-
- const hasSearchQuery = searchQuery.length > 0;
- const hasVideos = Number(videoList?.length) > 0;
- const hasChannels = Number(channelList?.length) > 0;
- const hasPlaylist = Number(playlistList?.length) > 0;
- const hasFulltext = Number(fulltextList?.length) > 0;
-
- const isSimpleQuery = queryType === 'simple';
- const isVideoQuery = queryType === 'video' || isSimpleQuery;
- const isChannelQuery = queryType === 'channel' || isSimpleQuery;
- const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
- const isFullTextQuery = queryType === 'full' || isSimpleQuery;
-
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- if (!hasSearchQuery) {
- setSearchResults(EmptySearchResponse);
-
- return;
- }
-
- const searchResults = await loadSearch(searchQuery);
-
- setSearchResults(searchResults);
- setRefresh(false);
- })();
- }, [searchQuery, refresh, hasSearchQuery]);
-
- return (
- <>
-
- TubeArchivist
-
- {showEmbeddedVideo && }
-
-
-
Search your Archive
-
-
-
- {
- setSearchQuery(event.target.value);
- }}
- />
-
-
-
- {hasSearchQuery && isVideoQuery && (
-
- )}
-
- {hasSearchQuery && isChannelQuery && (
-
-
Channel Results
-
-
-
-
- )}
-
- {hasSearchQuery && isPlaylistQuery && (
-
- )}
-
- {hasSearchQuery && isFullTextQuery && (
-
-
Fulltext Results
-
-
-
-
- )}
-
-
- {!hasVideos && !hasChannels && !hasPlaylist && !hasFulltext &&
}
-
- >
- );
-};
-
-export default Search;
+import { useLoaderData, useSearchParams } from 'react-router-dom';
+import { UserMeType } from '../api/actions/updateUserConfig';
+import { useEffect, useState } from 'react';
+import { VideoType, ViewLayoutType } from './Home';
+import loadSearch from '../api/loader/loadSearch';
+import { PlaylistType } from './Playlist';
+import { ChannelType } from './Channels';
+import VideoList from '../components/VideoList';
+import ChannelList from '../components/ChannelList';
+import PlaylistList from '../components/PlaylistList';
+import SubtitleList from '../components/SubtitleList';
+import { ViewStyles } from '../configuration/constants/ViewStyle';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import SearchExampleQueries from '../components/SearchExampleQueries';
+
+const EmptySearchResponse: SearchResultsType = {
+ results: {
+ video_results: [],
+ channel_results: [],
+ playlist_results: [],
+ fulltext_results: [],
+ },
+ queryType: 'simple',
+};
+
+type SearchResultType = {
+ video_results: VideoType[];
+ channel_results: ChannelType[];
+ playlist_results: PlaylistType[];
+ fulltext_results: [];
+};
+
+type SearchResultsType = {
+ results: SearchResultType;
+ queryType: string;
+};
+
+type SearchLoaderDataType = {
+ userConfig: UserMeType;
+};
+
+const Search = () => {
+ const { userConfig } = useLoaderData() as SearchLoaderDataType;
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+ const userMeConfig = userConfig.config;
+
+ const view = (userMeConfig.view_style_home || ViewStyles.grid) as ViewLayoutType;
+ const gridItems = userMeConfig.grid_items || 3;
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState();
+
+ const [refresh, setRefresh] = useState(false);
+
+ const videoList = searchResults?.results.video_results;
+ const channelList = searchResults?.results.channel_results;
+ const playlistList = searchResults?.results.playlist_results;
+ const fulltextList = searchResults?.results.fulltext_results;
+ const queryType = searchResults?.queryType;
+ const showEmbeddedVideo = videoId !== null;
+
+ const hasSearchQuery = searchQuery.length > 0;
+ const hasVideos = Number(videoList?.length) > 0;
+ const hasChannels = Number(channelList?.length) > 0;
+ const hasPlaylist = Number(playlistList?.length) > 0;
+ const hasFulltext = Number(fulltextList?.length) > 0;
+
+ const isSimpleQuery = queryType === 'simple';
+ const isVideoQuery = queryType === 'video' || isSimpleQuery;
+ const isChannelQuery = queryType === 'channel' || isSimpleQuery;
+ const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
+ const isFullTextQuery = queryType === 'full' || isSimpleQuery;
+
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (!hasSearchQuery) {
+ setSearchResults(EmptySearchResponse);
+
+ return;
+ }
+
+ const searchResults = await loadSearch(searchQuery);
+
+ setSearchResults(searchResults);
+ setRefresh(false);
+ })();
+ }, [searchQuery, refresh, hasSearchQuery]);
+
+ return (
+ <>
+ TubeArchivist
+ {showEmbeddedVideo && }
+
+
+
Search your Archive
+
+
+
+ {
+ setSearchQuery(event.target.value);
+ }}
+ />
+
+
+
+ {hasSearchQuery && isVideoQuery && (
+
+ )}
+
+ {hasSearchQuery && isChannelQuery && (
+
+
Channel Results
+
+
+
+
+ )}
+
+ {hasSearchQuery && isPlaylistQuery && (
+
+ )}
+
+ {hasSearchQuery && isFullTextQuery && (
+
+
Fulltext Results
+
+
+
+
+ )}
+
+
+ {!hasVideos && !hasChannels && !hasPlaylist && !hasFulltext &&
}
+
+ >
+ );
+};
+
+export default Search;
diff --git a/frontend/src/pages/SettingsActions.tsx b/frontend/src/pages/SettingsActions.tsx
index e89fe661..a3321a4a 100644
--- a/frontend/src/pages/SettingsActions.tsx
+++ b/frontend/src/pages/SettingsActions.tsx
@@ -1,245 +1,242 @@
-import { useEffect, useState } from 'react';
-import loadBackupList from '../api/loader/loadBackupList';
-import SettingsNavigation from '../components/SettingsNavigation';
-import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
-import updateTaskByName from '../api/actions/updateTaskByName';
-import queueBackup from '../api/actions/queueBackup';
-import restoreBackup from '../api/actions/restoreBackup';
-import Notifications from '../components/Notifications';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-
-type Backup = {
- filename: string;
- file_path: string;
- file_size: number;
- timestamp: string;
- reason: string;
-};
-
-type BackupListType = Backup[];
-
-const SettingsActions = () => {
- const [deleteIgnored, setDeleteIgnored] = useState(false);
- const [deletePending, setDeletePending] = useState(false);
- const [processingImports, setProcessingImports] = useState(false);
- const [reEmbed, setReEmbed] = useState(false);
- const [backupStarted, setBackupStarted] = useState(false);
- const [isRestoringBackup, setIsRestoringBackup] = useState(false);
- const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
-
- const [backupListResponse, setBackupListResponse] = useState();
-
- const backups = backupListResponse;
- const hasBackups = !!backups && backups?.length > 0;
-
- useEffect(() => {
- (async () => {
- const backupListResponse = await loadBackupList();
-
- setBackupListResponse(backupListResponse);
- })();
- }, []);
-
- return (
- <>
-
- TA | Actions
-
-
-
-
{
- setDeleteIgnored(false);
- setDeletePending(false);
- setProcessingImports(false);
- setReEmbed(false);
- setBackupStarted(false);
- setIsRestoringBackup(false);
- setReScanningFileSystem(false);
- }}
- />
-
-
-
Actions
-
-
-
Delete download queue
-
Delete your pending or previously ignored videos from your download queue.
- {deleteIgnored &&
Deleting download queue: ignored
}
- {!deleteIgnored && (
-
{
- await deleteDownloadQueueByFilter('ignore');
- setDeleteIgnored(true);
- }}
- />
- )}{' '}
- {deletePending && Deleting download queue: pending
}
- {!deletePending && (
- {
- await deleteDownloadQueueByFilter('pending');
- setDeletePending(true);
- }}
- />
- )}
-
-
-
Manual media files import.
-
- Add files to the cache/import folder. Make
- sure to follow the instructions in the Github{' '}
-
- Wiki
-
- .
-
-
- {processingImports &&
Processing import
}
- {!processingImports && (
-
{
- await updateTaskByName('manual_import');
- setProcessingImports(true);
- }}
- />
- )}
-
-
-
-
Embed thumbnails into media file.
-
Set extracted youtube thumbnail as cover art of the media file.
-
- {reEmbed &&
Processing thumbnails
}
- {!reEmbed && (
-
{
- await updateTaskByName('resync_thumbs');
- setReEmbed(true);
- }}
- />
- )}
-
-
-
-
ZIP file index backup
-
- Export your database to a zip file stored at{' '}
- cache/backup .
-
-
-
- Zip file backups are very slow for large archives and consistency is not guaranteed,
- use snapshots instead. Make sure no other tasks are running when creating a Zip file
- backup.
-
-
-
- {backupStarted &&
Backing up archive
}
- {!backupStarted && (
-
{
- await queueBackup();
- setBackupStarted(true);
- }}
- />
- )}
-
-
-
-
Restore from backup
-
- Danger Zone : This will replace your existing index
- with the backup.
-
-
- Restore from available backup files from{' '}
- cache/backup .
-
- {!hasBackups &&
No backups found.
}
- {hasBackups && (
- <>
-
-
- Timestamp
- Source
- Filename
-
- {isRestoringBackup &&
Restoring from backup
}
- {!isRestoringBackup &&
- backups.map(backup => {
- return (
-
- {
- await restoreBackup(backup.filename);
- setIsRestoringBackup(true);
- }}
- />
- {backup.timestamp}
- {backup.reason}
- {backup.filename}
-
- );
- })}
- >
- )}
-
-
-
Rescan filesystem
-
- Danger Zone : This will delete the metadata of
- deleted videos from the filesystem.
-
-
- Rescan your media folder looking for missing videos and clean up index. More infos on
- the Github{' '}
-
- Wiki
-
- .
-
-
- {reScanningFileSystem &&
File system scan in progress
}
- {!reScanningFileSystem && (
-
{
- await updateTaskByName('rescan_filesystem');
- setReScanningFileSystem(true);
- }}
- />
- )}
-
-
-
- >
- );
-};
-
-export default SettingsActions;
+import { useEffect, useState } from 'react';
+import loadBackupList from '../api/loader/loadBackupList';
+import SettingsNavigation from '../components/SettingsNavigation';
+import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
+import updateTaskByName from '../api/actions/updateTaskByName';
+import queueBackup from '../api/actions/queueBackup';
+import restoreBackup from '../api/actions/restoreBackup';
+import Notifications from '../components/Notifications';
+import Button from '../components/Button';
+
+type Backup = {
+ filename: string;
+ file_path: string;
+ file_size: number;
+ timestamp: string;
+ reason: string;
+};
+
+type BackupListType = Backup[];
+
+const SettingsActions = () => {
+ const [deleteIgnored, setDeleteIgnored] = useState(false);
+ const [deletePending, setDeletePending] = useState(false);
+ const [processingImports, setProcessingImports] = useState(false);
+ const [reEmbed, setReEmbed] = useState(false);
+ const [backupStarted, setBackupStarted] = useState(false);
+ const [isRestoringBackup, setIsRestoringBackup] = useState(false);
+ const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
+
+ const [backupListResponse, setBackupListResponse] = useState();
+
+ const backups = backupListResponse;
+ const hasBackups = !!backups && backups?.length > 0;
+
+ useEffect(() => {
+ (async () => {
+ const backupListResponse = await loadBackupList();
+
+ setBackupListResponse(backupListResponse);
+ })();
+ }, []);
+
+ return (
+ <>
+ TA | Actions
+
+
+
{
+ setDeleteIgnored(false);
+ setDeletePending(false);
+ setProcessingImports(false);
+ setReEmbed(false);
+ setBackupStarted(false);
+ setIsRestoringBackup(false);
+ setReScanningFileSystem(false);
+ }}
+ />
+
+
+
Actions
+
+
+
Delete download queue
+
Delete your pending or previously ignored videos from your download queue.
+ {deleteIgnored &&
Deleting download queue: ignored
}
+ {!deleteIgnored && (
+
{
+ await deleteDownloadQueueByFilter('ignore');
+ setDeleteIgnored(true);
+ }}
+ />
+ )}{' '}
+ {deletePending && Deleting download queue: pending
}
+ {!deletePending && (
+ {
+ await deleteDownloadQueueByFilter('pending');
+ setDeletePending(true);
+ }}
+ />
+ )}
+
+
+
Manual media files import.
+
+ Add files to the cache/import folder. Make
+ sure to follow the instructions in the Github{' '}
+
+ Wiki
+
+ .
+
+
+ {processingImports &&
Processing import
}
+ {!processingImports && (
+
{
+ await updateTaskByName('manual_import');
+ setProcessingImports(true);
+ }}
+ />
+ )}
+
+
+
+
Embed thumbnails into media file.
+
Set extracted youtube thumbnail as cover art of the media file.
+
+ {reEmbed &&
Processing thumbnails
}
+ {!reEmbed && (
+
{
+ await updateTaskByName('resync_thumbs');
+ setReEmbed(true);
+ }}
+ />
+ )}
+
+
+
+
ZIP file index backup
+
+ Export your database to a zip file stored at{' '}
+ cache/backup .
+
+
+
+ Zip file backups are very slow for large archives and consistency is not guaranteed,
+ use snapshots instead. Make sure no other tasks are running when creating a Zip file
+ backup.
+
+
+
+ {backupStarted &&
Backing up archive
}
+ {!backupStarted && (
+
{
+ await queueBackup();
+ setBackupStarted(true);
+ }}
+ />
+ )}
+
+
+
+
Restore from backup
+
+ Danger Zone : This will replace your existing index
+ with the backup.
+
+
+ Restore from available backup files from{' '}
+ cache/backup .
+
+ {!hasBackups &&
No backups found.
}
+ {hasBackups && (
+ <>
+
+
+ Timestamp
+ Source
+ Filename
+
+ {isRestoringBackup &&
Restoring from backup
}
+ {!isRestoringBackup &&
+ backups.map(backup => {
+ return (
+
+ {
+ await restoreBackup(backup.filename);
+ setIsRestoringBackup(true);
+ }}
+ />
+ {backup.timestamp}
+ {backup.reason}
+ {backup.filename}
+
+ );
+ })}
+ >
+ )}
+
+
+
Rescan filesystem
+
+ Danger Zone : This will delete the metadata of
+ deleted videos from the filesystem.
+
+
+ Rescan your media folder looking for missing videos and clean up index. More infos on
+ the Github{' '}
+
+ Wiki
+
+ .
+
+
+ {reScanningFileSystem &&
File system scan in progress
}
+ {!reScanningFileSystem && (
+
{
+ await updateTaskByName('rescan_filesystem');
+ setReScanningFileSystem(true);
+ }}
+ />
+ )}
+
+
+
+ >
+ );
+};
+
+export default SettingsActions;
diff --git a/frontend/src/pages/SettingsApplication.tsx b/frontend/src/pages/SettingsApplication.tsx
index 11c27762..2732ef96 100644
--- a/frontend/src/pages/SettingsApplication.tsx
+++ b/frontend/src/pages/SettingsApplication.tsx
@@ -1,969 +1,965 @@
-import { useEffect, useState } from 'react';
-import loadSnapshots from '../api/loader/loadSnapshots';
-import Notifications from '../components/Notifications';
-import PaginationDummy from '../components/PaginationDummy';
-import SettingsNavigation from '../components/SettingsNavigation';
-import restoreSnapshot from '../api/actions/restoreSnapshot';
-import queueSnapshot from '../api/actions/queueSnapshot';
-import updateCookie, { ValidatedCookieType } from '../api/actions/updateCookie';
-import deleteApiToken from '../api/actions/deleteApiToken';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-import loadAppsettingsConfig, { AppSettingsConfigType } from '../api/loader/loadAppsettingsConfig';
-import updateAppsettingsConfig from '../api/actions/updateAppsettingsConfig';
-import loadApiToken from '../api/loader/loadApiToken';
-
-type SnapshotType = {
- id: string;
- state: string;
- es_version: string;
- start_date: string;
- end_date: string;
- end_stamp: number;
- duration_s: number;
-};
-
-type SnapshotListType = {
- next_exec: number;
- next_exec_str: string;
- expire_after: string;
- snapshots?: SnapshotType[];
-};
-
-type SettingsApplicationReponses = {
- snapshots?: SnapshotListType;
- appSettingsConfig?: AppSettingsConfigType;
- apiToken: string;
-};
-
-const SettingsApplication = () => {
- const [response, setResponse] = useState({
- snapshots: undefined,
- appSettingsConfig: undefined,
- apiToken: '',
- });
-
- const snapshots = response?.snapshots;
- const appSettingsConfig = response?.appSettingsConfig;
- const apiToken = response.apiToken;
-
- // Subscriptions
- const [videoPageSize, setVideoPageSize] = useState(
- appSettingsConfig?.subscriptions.channel_size || 0,
- );
- const [livePageSize, setLivePageSize] = useState(
- appSettingsConfig?.subscriptions.live_channel_size || 0,
- );
- const [shortPageSize, setShortPageSize] = useState(
- appSettingsConfig?.subscriptions.shorts_channel_size || 0,
- );
- const [isAutostart, setIsAutostart] = useState(
- appSettingsConfig?.subscriptions.auto_start || false,
- );
-
- // Downloads
- const [currentDownloadSpeed, setCurrentDownloadSpeed] = useState(
- appSettingsConfig?.downloads.limit_speed || 0,
- );
- const [currentThrottledRate, setCurrentThrottledRate] = useState(
- appSettingsConfig?.downloads.throttledratelimit || 0,
- );
- const [currentScrapingSleep, setCurrentScrapingSleep] = useState(
- appSettingsConfig?.downloads.sleep_interval || 0,
- );
- const [currentAutodelete, setCurrentAutodelete] = useState(
- appSettingsConfig?.downloads.autodelete_days || 0,
- );
-
- // Download Format
- const [downloadsFormat, setDownloadsFormat] = useState(appSettingsConfig?.downloads.format || '');
- const [downloadsFormatSort, setDownloadsFormatSort] = useState(
- appSettingsConfig?.downloads.format_sort || '',
- );
- const [downloadsExtractorLang, setDownloadsExtractorLang] = useState(
- appSettingsConfig?.downloads.extractor_lang || '',
- );
- const [embedMetadata, setEmbedMetadata] = useState(
- appSettingsConfig?.downloads.add_metadata || false,
- );
- const [embedThumbnail, setEmbedThumbnail] = useState(
- appSettingsConfig?.downloads.add_thumbnail || false,
- );
-
- // Subtitles
- const [subtitleLang, setSubtitleLang] = useState(appSettingsConfig?.downloads.subtitle || '');
- const [subtitleSource, setSubtitleSource] = useState(
- appSettingsConfig?.downloads.subtitle_source || '',
- );
- const [indexSubtitles, setIndexSubtitles] = useState(
- appSettingsConfig?.downloads.subtitle_index || false,
- );
-
- // Comments
- const [commentsMax, setCommentsMax] = useState(appSettingsConfig?.downloads.comment_max || 0);
- const [commentsSort, setCommentsSort] = useState(appSettingsConfig?.downloads.comment_sort || '');
-
- // Cookie
- const [cookieImport, setCookieImport] = useState(
- appSettingsConfig?.downloads.cookie_import || false,
- );
- const [validatingCookie, setValidatingCookie] = useState(false);
- const [cookieResponse, setCookieResponse] = useState();
-
- // Integrations
- const [showApiToken, setShowApiToken] = useState(false);
- const [downloadDislikes, setDownloadDislikes] = useState(
- appSettingsConfig?.downloads.integrate_ryd || false,
- );
- const [enableSponsorBlock, setEnableSponsorBlock] = useState(
- appSettingsConfig?.downloads.integrate_sponsorblock || false,
- );
- const [resetTokenResponse, setResetTokenResponse] = useState({});
-
- // Snapshots
- const [enableSnapshots, setEnableSnapshots] = useState(
- appSettingsConfig?.application.enable_snapshot || false,
- );
- const [isSnapshotQueued, setIsSnapshotQueued] = useState(false);
- const [restoringSnapshot, setRestoringSnapshot] = useState(false);
-
- const onSubmit = async () => {
- return await updateAppsettingsConfig({
- application: {
- enable_snapshot: enableSnapshots,
- },
- downloads: {
- limit_speed: currentDownloadSpeed,
- sleep_interval: currentScrapingSleep,
- autodelete_days: currentAutodelete,
- format: downloadsFormat,
- format_sort: downloadsFormatSort,
- add_metadata: embedMetadata,
- add_thumbnail: embedThumbnail,
- subtitle: subtitleLang,
- subtitle_source: subtitleSource,
- subtitle_index: indexSubtitles,
- comment_max: commentsMax,
- comment_sort: commentsSort,
- cookie_import: cookieImport,
- throttledratelimit: currentThrottledRate,
- extractor_lang: downloadsExtractorLang,
- integrate_ryd: downloadDislikes,
- integrate_sponsorblock: enableSponsorBlock,
- },
- subscriptions: {
- auto_start: isAutostart,
- channel_size: videoPageSize,
- live_channel_size: livePageSize,
- shorts_channel_size: shortPageSize,
- },
- });
- };
-
- useEffect(() => {
- (async () => {
- const snapshotResponse = await loadSnapshots();
- const appSettingsConfig = await loadAppsettingsConfig();
- const apiToken = await loadApiToken();
-
- setResponse({
- snapshots: snapshotResponse,
- appSettingsConfig,
- apiToken: apiToken.token,
- });
- })();
- }, []);
-
- return (
- <>
-
- TA | Application Settings
-
-
-
-
-
-
-
Application Configurations
-
-
-
-
-
- >
- );
-};
-
-export default SettingsApplication;
+import { useEffect, useState } from 'react';
+import loadSnapshots from '../api/loader/loadSnapshots';
+import Notifications from '../components/Notifications';
+import PaginationDummy from '../components/PaginationDummy';
+import SettingsNavigation from '../components/SettingsNavigation';
+import restoreSnapshot from '../api/actions/restoreSnapshot';
+import queueSnapshot from '../api/actions/queueSnapshot';
+import updateCookie, { ValidatedCookieType } from '../api/actions/updateCookie';
+import deleteApiToken from '../api/actions/deleteApiToken';
+import Button from '../components/Button';
+import loadAppsettingsConfig, { AppSettingsConfigType } from '../api/loader/loadAppsettingsConfig';
+import updateAppsettingsConfig from '../api/actions/updateAppsettingsConfig';
+import loadApiToken from '../api/loader/loadApiToken';
+
+type SnapshotType = {
+ id: string;
+ state: string;
+ es_version: string;
+ start_date: string;
+ end_date: string;
+ end_stamp: number;
+ duration_s: number;
+};
+
+type SnapshotListType = {
+ next_exec: number;
+ next_exec_str: string;
+ expire_after: string;
+ snapshots?: SnapshotType[];
+};
+
+type SettingsApplicationReponses = {
+ snapshots?: SnapshotListType;
+ appSettingsConfig?: AppSettingsConfigType;
+ apiToken?: string;
+};
+
+const SettingsApplication = () => {
+ const [response, setResponse] = useState();
+ const [refresh, setRefresh] = useState(false);
+
+ const snapshots = response?.snapshots;
+ const appSettingsConfig = response?.appSettingsConfig;
+ const apiToken = response?.apiToken;
+
+ // Subscriptions
+ const [videoPageSize, setVideoPageSize] = useState(0);
+ const [livePageSize, setLivePageSize] = useState(0);
+ const [shortPageSize, setShortPageSize] = useState(0);
+ const [isAutostart, setIsAutostart] = useState(false);
+
+ // Downloads
+ const [currentDownloadSpeed, setCurrentDownloadSpeed] = useState(0);
+ const [currentThrottledRate, setCurrentThrottledRate] = useState(0);
+ const [currentScrapingSleep, setCurrentScrapingSleep] = useState(0);
+ const [currentAutodelete, setCurrentAutodelete] = useState(0);
+
+ // Download Format
+ const [downloadsFormat, setDownloadsFormat] = useState('');
+ const [downloadsFormatSort, setDownloadsFormatSort] = useState('');
+ const [downloadsExtractorLang, setDownloadsExtractorLang] = useState('');
+ const [embedMetadata, setEmbedMetadata] = useState(false);
+ const [embedThumbnail, setEmbedThumbnail] = useState(false);
+
+ // Subtitles
+ const [subtitleLang, setSubtitleLang] = useState('');
+ const [subtitleSource, setSubtitleSource] = useState('');
+ const [indexSubtitles, setIndexSubtitles] = useState(false);
+
+ // Comments
+ const [commentsMax, setCommentsMax] = useState('');
+ const [commentsSort, setCommentsSort] = useState('');
+
+ // Cookie
+ const [cookieImport, setCookieImport] = useState(false);
+ const [validatingCookie, setValidatingCookie] = useState(false);
+ const [cookieResponse, setCookieResponse] = useState();
+
+ // Integrations
+ const [showApiToken, setShowApiToken] = useState(false);
+ const [downloadDislikes, setDownloadDislikes] = useState(false);
+ const [enableSponsorBlock, setEnableSponsorBlock] = useState(false);
+ const [resetTokenResponse, setResetTokenResponse] = useState({});
+
+ // Snapshots
+ const [enableSnapshots, setEnableSnapshots] = useState(false);
+ const [isSnapshotQueued, setIsSnapshotQueued] = useState(false);
+ const [restoringSnapshot, setRestoringSnapshot] = useState(false);
+
+ const onSubmit = async () => {
+ await updateAppsettingsConfig({
+ application: {
+ enable_snapshot: enableSnapshots,
+ },
+ downloads: {
+ limit_speed: currentDownloadSpeed,
+ sleep_interval: currentScrapingSleep,
+ autodelete_days: currentAutodelete,
+ format: downloadsFormat,
+ format_sort: downloadsFormatSort,
+ add_metadata: embedMetadata,
+ add_thumbnail: embedThumbnail,
+ subtitle: subtitleLang,
+ subtitle_source: subtitleSource,
+ subtitle_index: indexSubtitles,
+ comment_max: commentsMax,
+ comment_sort: commentsSort,
+ cookie_import: cookieImport,
+ throttledratelimit: currentThrottledRate,
+ extractor_lang: downloadsExtractorLang,
+ integrate_ryd: downloadDislikes,
+ integrate_sponsorblock: enableSponsorBlock,
+ },
+ subscriptions: {
+ auto_start: isAutostart,
+ channel_size: videoPageSize,
+ live_channel_size: livePageSize,
+ shorts_channel_size: shortPageSize,
+ },
+ });
+
+ setRefresh(true);
+ };
+
+ const fetchData = async () => {
+ const snapshotResponse = await loadSnapshots();
+ const appSettingsConfig = await loadAppsettingsConfig();
+ const apiToken = await loadApiToken();
+
+ // Subscriptions
+ setVideoPageSize(appSettingsConfig?.subscriptions.channel_size);
+ setLivePageSize(appSettingsConfig?.subscriptions.live_channel_size);
+ setShortPageSize(appSettingsConfig?.subscriptions.shorts_channel_size);
+ setIsAutostart(appSettingsConfig?.subscriptions.auto_start);
+
+ // Downloads
+ setCurrentDownloadSpeed(appSettingsConfig?.downloads.limit_speed || 0);
+ setCurrentThrottledRate(appSettingsConfig?.downloads.throttledratelimit || 0);
+ setCurrentScrapingSleep(appSettingsConfig?.downloads.sleep_interval);
+ setCurrentAutodelete(appSettingsConfig?.downloads.autodelete_days);
+
+ // Download Format
+ setDownloadsFormat(appSettingsConfig?.downloads.format.toString());
+ setDownloadsFormatSort(appSettingsConfig?.downloads.format_sort.toString());
+ setDownloadsExtractorLang(appSettingsConfig?.downloads.extractor_lang.toString());
+ setEmbedMetadata(appSettingsConfig?.downloads.add_metadata);
+ setEmbedThumbnail(appSettingsConfig?.downloads.add_thumbnail);
+
+ // Subtitles
+ setSubtitleLang(appSettingsConfig?.downloads.subtitle.toString());
+ setSubtitleSource(appSettingsConfig?.downloads.subtitle_source.toString());
+ setIndexSubtitles(appSettingsConfig?.downloads.subtitle_index);
+
+ // Comments
+ setCommentsMax(appSettingsConfig?.downloads.comment_max.toString());
+ setCommentsSort(appSettingsConfig?.downloads.comment_sort);
+
+ // Cookie
+ setCookieImport(appSettingsConfig?.downloads.cookie_import);
+
+ // Integrations
+ setDownloadDislikes(appSettingsConfig?.downloads.integrate_ryd);
+ setEnableSponsorBlock(appSettingsConfig?.downloads.integrate_sponsorblock);
+
+ // Snapshots
+ setEnableSnapshots(appSettingsConfig?.application.enable_snapshot);
+
+ setResponse({
+ snapshots: snapshotResponse,
+ appSettingsConfig,
+ apiToken: apiToken.token,
+ });
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ if (refresh) {
+ fetchData();
+ setRefresh(false);
+ }
+ }, [refresh]);
+
+ return (
+ <>
+ TA | Application Settings
+
+
+
+
+
+
Application Configurations
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsApplication;
diff --git a/frontend/src/pages/SettingsDashboard.tsx b/frontend/src/pages/SettingsDashboard.tsx
index fd1f98d3..1f58499c 100644
--- a/frontend/src/pages/SettingsDashboard.tsx
+++ b/frontend/src/pages/SettingsDashboard.tsx
@@ -1,263 +1,260 @@
-import { useEffect, useState } from 'react';
-import SettingsNavigation from '../components/SettingsNavigation';
-import loadStatsVideo from '../api/loader/loadStatsVideo';
-import loadStatsChannel from '../api/loader/loadStatsChannel';
-import loadStatsPlaylist from '../api/loader/loadStatsPlaylist';
-import loadStatsDownload from '../api/loader/loadStatsDownload';
-import loadStatsWatchProgress from '../api/loader/loadStatsWatchProgress';
-import loadStatsDownloadHistory from '../api/loader/loadStatsDownloadHistory';
-import loadStatsBiggestChannels from '../api/loader/loadStatsBiggestChannels';
-import OverviewStats from '../components/OverviewStats';
-import VideoTypeStats from '../components/VideoTypeStats';
-import ApplicationStats from '../components/ApplicationStats';
-import WatchProgressStats from '../components/WatchProgressStats';
-import DownloadHistoryStats from '../components/DownloadHistoryStats';
-import BiggestChannelsStats from '../components/BiggestChannelsStats';
-import Notifications from '../components/Notifications';
-import PaginationDummy from '../components/PaginationDummy';
-import { Helmet } from 'react-helmet';
-
-export type VideoStatsType = {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- type_videos: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- type_shorts: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- active_true: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- active_false: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- type_streams: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
-};
-
-export type ChannelStatsType = {
- doc_count: number;
- active_true: number;
- subscribed_true: number;
-};
-
-export type PlaylistStatsType = {
- doc_count: number;
- active_false: number;
- active_true: number;
- subscribed_true: number;
-};
-
-export type DownloadStatsType = {
- pending: number;
- pending_videos: number;
- pending_shorts: number;
- pending_streams: number;
-};
-
-export type WatchProgressStatsType = {
- total: {
- duration: number;
- duration_str: string;
- items: number;
- };
- unwatched: {
- duration: number;
- duration_str: string;
- progress: number;
- items: number;
- };
- watched: {
- duration: number;
- duration_str: string;
- progress: number;
- items: number;
- };
-};
-
-type DownloadHistoryType = {
- date: string;
- count: number;
- media_size: number;
-};
-
-export type DownloadHistoryStatsType = DownloadHistoryType[];
-
-type BiggestChannelsType = {
- id: string;
- name: string;
- doc_count: number;
- duration: number;
- duration_str: string;
- media_size: number;
-};
-
-export type BiggestChannelsStatsType = BiggestChannelsType[];
-
-type DashboardStatsReponses = {
- videoStats?: VideoStatsType;
- channelStats?: ChannelStatsType;
- playlistStats?: PlaylistStatsType;
- downloadStats?: DownloadStatsType;
- watchProgressStats?: WatchProgressStatsType;
- downloadHistoryStats?: DownloadHistoryStatsType;
- biggestChannelsStatsByCount?: BiggestChannelsStatsType;
- biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
- biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
-};
-
-const SettingsDashboard = () => {
- const [useSi, setUseSi] = useState(false);
-
- const [response, setResponse] = useState({
- videoStats: undefined,
- });
-
- const videoStats = response?.videoStats;
- const channelStats = response?.channelStats;
- const playlistStats = response?.playlistStats;
- const downloadStats = response?.downloadStats;
- const watchProgressStats = response?.watchProgressStats;
- const downloadHistoryStats = response?.downloadHistoryStats;
- const biggestChannelsStatsByCount = response?.biggestChannelsStatsByCount;
- const biggestChannelsStatsByDuration = response?.biggestChannelsStatsByDuration;
- const biggestChannelsStatsByMediaSize = response?.biggestChannelsStatsByMediaSize;
-
- useEffect(() => {
- (async () => {
- const all = await Promise.all([
- await loadStatsVideo(),
- await loadStatsChannel(),
- await loadStatsPlaylist(),
- await loadStatsDownload(),
- await loadStatsWatchProgress(),
- await loadStatsDownloadHistory(),
- await loadStatsBiggestChannels('doc_count'),
- await loadStatsBiggestChannels('duration'),
- await loadStatsBiggestChannels('media_size'),
- ]);
-
- const [
- videoStats,
- channelStats,
- playlistStats,
- downloadStats,
- watchProgressStats,
- downloadHistoryStats,
- biggestChannelsStatsByCount,
- biggestChannelsStatsByDuration,
- biggestChannelsStatsByMediaSize,
- ] = all;
-
- setResponse({
- videoStats,
- channelStats,
- playlistStats,
- downloadStats,
- watchProgressStats,
- downloadHistoryStats,
- biggestChannelsStatsByCount,
- biggestChannelsStatsByDuration,
- biggestChannelsStatsByMediaSize,
- });
- })();
- }, []);
-
- return (
- <>
-
- TA | Settings Dashboard
-
-
-
-
-
-
Your Archive
-
-
- File Sizes in:
- {
- const value = event.target.value;
- console.log(value);
- setUseSi(value === 'true');
- }}
- >
- SI units
- Binary units
-
-
-
-
-
-
-
-
-
Download History
-
-
-
-
-
-
Biggest Channels
-
-
-
-
-
-
-
- >
- );
-};
-
-export default SettingsDashboard;
+import { useEffect, useState } from 'react';
+import SettingsNavigation from '../components/SettingsNavigation';
+import loadStatsVideo from '../api/loader/loadStatsVideo';
+import loadStatsChannel from '../api/loader/loadStatsChannel';
+import loadStatsPlaylist from '../api/loader/loadStatsPlaylist';
+import loadStatsDownload from '../api/loader/loadStatsDownload';
+import loadStatsWatchProgress from '../api/loader/loadStatsWatchProgress';
+import loadStatsDownloadHistory from '../api/loader/loadStatsDownloadHistory';
+import loadStatsBiggestChannels from '../api/loader/loadStatsBiggestChannels';
+import OverviewStats from '../components/OverviewStats';
+import VideoTypeStats from '../components/VideoTypeStats';
+import ApplicationStats from '../components/ApplicationStats';
+import WatchProgressStats from '../components/WatchProgressStats';
+import DownloadHistoryStats from '../components/DownloadHistoryStats';
+import BiggestChannelsStats from '../components/BiggestChannelsStats';
+import Notifications from '../components/Notifications';
+import PaginationDummy from '../components/PaginationDummy';
+
+export type VideoStatsType = {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ type_videos: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ type_shorts: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ active_true: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ active_false: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ type_streams: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+};
+
+export type ChannelStatsType = {
+ doc_count: number;
+ active_true: number;
+ subscribed_true: number;
+};
+
+export type PlaylistStatsType = {
+ doc_count: number;
+ active_false: number;
+ active_true: number;
+ subscribed_true: number;
+};
+
+export type DownloadStatsType = {
+ pending: number;
+ pending_videos: number;
+ pending_shorts: number;
+ pending_streams: number;
+};
+
+export type WatchProgressStatsType = {
+ total: {
+ duration: number;
+ duration_str: string;
+ items: number;
+ };
+ unwatched: {
+ duration: number;
+ duration_str: string;
+ progress: number;
+ items: number;
+ };
+ watched: {
+ duration: number;
+ duration_str: string;
+ progress: number;
+ items: number;
+ };
+};
+
+type DownloadHistoryType = {
+ date: string;
+ count: number;
+ media_size: number;
+};
+
+export type DownloadHistoryStatsType = DownloadHistoryType[];
+
+type BiggestChannelsType = {
+ id: string;
+ name: string;
+ doc_count: number;
+ duration: number;
+ duration_str: string;
+ media_size: number;
+};
+
+export type BiggestChannelsStatsType = BiggestChannelsType[];
+
+type DashboardStatsReponses = {
+ videoStats?: VideoStatsType;
+ channelStats?: ChannelStatsType;
+ playlistStats?: PlaylistStatsType;
+ downloadStats?: DownloadStatsType;
+ watchProgressStats?: WatchProgressStatsType;
+ downloadHistoryStats?: DownloadHistoryStatsType;
+ biggestChannelsStatsByCount?: BiggestChannelsStatsType;
+ biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
+ biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
+};
+
+const SettingsDashboard = () => {
+ const [useSi, setUseSi] = useState(false);
+
+ const [response, setResponse] = useState({
+ videoStats: undefined,
+ });
+
+ const videoStats = response?.videoStats;
+ const channelStats = response?.channelStats;
+ const playlistStats = response?.playlistStats;
+ const downloadStats = response?.downloadStats;
+ const watchProgressStats = response?.watchProgressStats;
+ const downloadHistoryStats = response?.downloadHistoryStats;
+ const biggestChannelsStatsByCount = response?.biggestChannelsStatsByCount;
+ const biggestChannelsStatsByDuration = response?.biggestChannelsStatsByDuration;
+ const biggestChannelsStatsByMediaSize = response?.biggestChannelsStatsByMediaSize;
+
+ useEffect(() => {
+ (async () => {
+ const all = await Promise.all([
+ await loadStatsVideo(),
+ await loadStatsChannel(),
+ await loadStatsPlaylist(),
+ await loadStatsDownload(),
+ await loadStatsWatchProgress(),
+ await loadStatsDownloadHistory(),
+ await loadStatsBiggestChannels('doc_count'),
+ await loadStatsBiggestChannels('duration'),
+ await loadStatsBiggestChannels('media_size'),
+ ]);
+
+ const [
+ videoStats,
+ channelStats,
+ playlistStats,
+ downloadStats,
+ watchProgressStats,
+ downloadHistoryStats,
+ biggestChannelsStatsByCount,
+ biggestChannelsStatsByDuration,
+ biggestChannelsStatsByMediaSize,
+ ] = all;
+
+ setResponse({
+ videoStats,
+ channelStats,
+ playlistStats,
+ downloadStats,
+ watchProgressStats,
+ downloadHistoryStats,
+ biggestChannelsStatsByCount,
+ biggestChannelsStatsByDuration,
+ biggestChannelsStatsByMediaSize,
+ });
+ })();
+ }, []);
+
+ return (
+ <>
+ TA | Settings Dashboard
+
+
+
+
+
Your Archive
+
+
+ File Sizes in:
+ {
+ const value = event.target.value;
+ console.log(value);
+ setUseSi(value === 'true');
+ }}
+ >
+ SI units
+ Binary units
+
+
+
+
+
+
+
+
+
Download History
+
+
+
+
+
+
Biggest Channels
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsDashboard;
diff --git a/frontend/src/pages/SettingsScheduling.tsx b/frontend/src/pages/SettingsScheduling.tsx
index 6709a8ef..a73728e7 100644
--- a/frontend/src/pages/SettingsScheduling.tsx
+++ b/frontend/src/pages/SettingsScheduling.tsx
@@ -1,462 +1,496 @@
-import { Helmet } from 'react-helmet';
-import Notifications from '../components/Notifications';
-import SettingsNavigation from '../components/SettingsNavigation';
-import Button from '../components/Button';
-
-type CronTabType = {
- minute: number;
- hour: number;
- day_of_week: number;
-};
-
-type SchedulerErrorType = {
- errors: string[];
-};
-
-type NotificationItemType = {
- task: string;
- notification: {
- title: string;
- urls: string[];
- };
-};
-
-type SettingsSchedulingResponseType = {
- update_subscribed: {
- crontab: CronTabType;
- };
- check_reindex: {
- crontab: CronTabType;
- task_config: {
- days: 0;
- };
- };
- thumbnail_check: {
- crontab: CronTabType;
- };
- download_pending: {
- crontab: CronTabType;
- };
- run_backup: {
- crontab: CronTabType;
- task_config: {
- rotate: false;
- };
- };
- notifications: {
- items: NotificationItemType[];
- };
- scheduler_form: {
- update_subscribed: SchedulerErrorType;
- download_pending: SchedulerErrorType;
- check_reindex: SchedulerErrorType;
- thumbnail_check: SchedulerErrorType;
- run_backup: SchedulerErrorType;
- };
-};
-
-const SettingsScheduling = () => {
- const response: SettingsSchedulingResponseType = {
- update_subscribed: {
- crontab: {
- minute: 0,
- hour: 0,
- day_of_week: 0,
- },
- },
- check_reindex: {
- crontab: {
- minute: 0,
- hour: 0,
- day_of_week: 0,
- },
- task_config: {
- days: 0,
- },
- },
- thumbnail_check: {
- crontab: {
- minute: 0,
- hour: 0,
- day_of_week: 0,
- },
- },
- download_pending: {
- crontab: {
- minute: 0,
- hour: 0,
- day_of_week: 0,
- },
- },
- run_backup: {
- crontab: {
- minute: 0,
- hour: 0,
- day_of_week: 0,
- },
- task_config: {
- rotate: false,
- },
- },
- notifications: {
- items: [
- {
- task: '',
- notification: {
- title: '',
- urls: [''],
- },
- },
- ],
- },
- scheduler_form: {
- update_subscribed: {
- errors: ['error?'],
- },
- download_pending: {
- errors: ['error?'],
- },
- check_reindex: {
- errors: ['error?'],
- },
- thumbnail_check: {
- errors: ['error?'],
- },
- run_backup: {
- errors: ['error?'],
- },
- },
- };
-
- const {
- check_reindex,
- download_pending,
- notifications,
- run_backup,
- scheduler_form,
- thumbnail_check,
- update_subscribed,
- } = response;
-
- return (
- <>
-
- TA | Scheduling Settings
-
-
-
-
-
-
-
Scheduler Setup
-
-
- Schedule settings expect a cron like format, where the first value is minute, second
- is hour and third is day of the week.
-
-
Examples:
-
-
- 0 15 * : Run task every day at 15:00 in the
- afternoon.
-
-
- 30 8 */2 : Run task every second day of the
- week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
-
-
- auto : Sensible default.
-
-
-
Note:
-
-
- 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
- hour.
-
-
-
-
-
-
- >
- );
-};
-
-export default SettingsScheduling;
+import Notifications from '../components/Notifications';
+import SettingsNavigation from '../components/SettingsNavigation';
+import Button from '../components/Button';
+import PaginationDummy from '../components/PaginationDummy';
+import { useEffect, useState } from 'react';
+import loadSchedule, { ScheduleResponseType } from '../api/loader/loadSchedule';
+import loadAppriseNotification, {
+ AppriseNotificationType,
+} from '../api/loader/loadAppriseNotification';
+import deleteTaskSchedule from '../api/actions/deleteTaskSchedule';
+import createTaskSchedule from '../api/actions/createTaskSchedule';
+import createAppriseNotificationUrl, {
+ AppriseTaskNameType,
+} from '../api/actions/createAppriseNotificationUrl';
+import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
+
+const SettingsScheduling = () => {
+ const [refresh, setRefresh] = useState(false);
+
+ const [scheduleResponse, setScheduleResponse] = useState([]);
+ const [appriseNotification, setAppriseNotification] = useState();
+
+ const [updateSubscribed, setUpdateSubscribed] = useState();
+ const [downloadPending, setDownloadPending] = useState();
+ const [checkReindex, setCheckReindex] = useState();
+ const [checkReindexDays, setCheckReindexDays] = useState(undefined);
+ const [thumbnailCheck, setThumbnailCheck] = useState();
+ const [zipBackup, setZipBackup] = useState();
+ const [zipBackupDays, setZipBackupDays] = useState(undefined);
+ const [notificationUrl, setNotificationUrl] = useState(undefined);
+ const [notificationTask, setNotificationTask] = useState('');
+
+ useEffect(() => {
+ (async () => {
+ if (refresh) {
+ const scheduleResponse = await loadSchedule();
+ const appriseNotificationResponse = await loadAppriseNotification();
+
+ setScheduleResponse(scheduleResponse);
+ setAppriseNotification(appriseNotificationResponse);
+
+ setRefresh(false);
+ }
+ })();
+ }, [refresh]);
+
+ useEffect(() => {
+ setRefresh(true);
+ }, []);
+
+ const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
+
+ console.log(groupedSchedules);
+
+ const { update_subscribed, download_pending, run_backup, check_reindex, thumbnail_check } =
+ groupedSchedules;
+
+ const updateSubscribedSchedule = update_subscribed?.pop();
+ const downloadPendingSchedule = download_pending?.pop();
+ const runBackup = run_backup?.pop();
+ const checkReindexSchedule = check_reindex?.pop();
+ const thumbnailCheckSchedule = thumbnail_check?.pop();
+
+ return (
+ <>
+ TA | Scheduling Settings
+
+
+
+
+
+
Scheduler Setup
+
+
+ Schedule settings expect a cron like format, where the first value is minute, second
+ is hour and third is day of the week.
+
+
Examples:
+
+
+ 0 15 * : Run task every day at 15:00 in the
+ afternoon.
+
+
+ 30 8 */2 : Run task every second day of the
+ week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
+
+
+ auto : Sensible default.
+
+
+
Note:
+
+
+ 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
+ hour.
+
+
+
+
+
+
+
Rescan Subscriptions
+
+
+ Become a sponsor and join{' '}
+
+ members.tubearchivist.com
+ {' '}
+ to get access to real time notifications for
+ new videos uploaded by your favorite channels.
+
+
+ Current rescan schedule:{' '}
+
+ {!updateSubscribedSchedule && 'False'}
+ {updateSubscribedSchedule && (
+ <>
+ {updateSubscribedSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('update_subscribed');
+
+ setRefresh(true);
+ }}
+ className="danger-button"
+ />
+ >
+ )}
+
+
+
Periodically rescan your subscriptions:
+
+
{
+ setUpdateSubscribed(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('update_subscribed', {
+ schedule: updateSubscribed,
+ });
+
+ setUpdateSubscribed('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
Start Download
+
+
+ Current Download schedule:{' '}
+
+ {!download_pending && 'False'}
+ {downloadPendingSchedule && (
+ <>
+ {downloadPendingSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('download_pending');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Automatic video download schedule:
+
+
{
+ setDownloadPending(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('download_pending', {
+ schedule: downloadPending,
+ });
+
+ setDownloadPending('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
+
Refresh Metadata
+
+
+ Current Metadata refresh schedule:{' '}
+
+ {!checkReindexSchedule && 'False'}
+ {checkReindexSchedule && (
+ <>
+ {checkReindexSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('check_reindex');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Daily schedule to refresh metadata from YouTube:
+
+
{
+ setCheckReindex(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('check_reindex', {
+ schedule: checkReindex,
+ });
+
+ setCheckReindex('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+ Current refresh for metadata older than x days:{' '}
+ {checkReindexSchedule?.config?.days}
+
+
Refresh older than x days, recommended 90:
+
+
{
+ setCheckReindexDays(Number(e.currentTarget.value));
+ }}
+ />
+
{
+ await createTaskSchedule('check_reindex', {
+ config: {
+ days: checkReindexDays,
+ },
+ });
+
+ setCheckReindexDays(undefined);
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
+
Thumbnail Check
+
+
+ Current thumbnail check schedule:{' '}
+
+ {!thumbnailCheckSchedule && 'False'}
+ {thumbnailCheckSchedule && (
+ <>
+ {thumbnailCheckSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('thumbnail_check');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Periodically check and cleanup thumbnails:
+
+
{
+ setThumbnailCheck(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('thumbnail_check', {
+ schedule: thumbnailCheck,
+ });
+
+ setThumbnailCheck('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
ZIP file index backup
+
+
+
+ Zip file backups are very slow for large archives and consistency is not guaranteed,
+ use snapshots instead. Make sure no other tasks are running when creating a Zip file
+ backup.
+
+
+
+ Current index backup schedule:{' '}
+
+ {!runBackup && 'False'}
+ {runBackup && (
+ <>
+ {runBackup.schedule}{' '}
+ {
+ await deleteTaskSchedule('run_backup');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Automatically backup metadata to a zip file:
+
+
{
+ setZipBackup(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('run_backup', {
+ schedule: zipBackup,
+ });
+
+ setZipBackup('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+ Current backup files to keep:{' '}
+ {runBackup?.config?.rotate}
+
+
Max auto backups to keep:
+
+
{
+ setZipBackupDays(Number(e.currentTarget.value));
+ }}
+ />
+
{
+ await createTaskSchedule('run_backup', {
+ config: {
+ rotate: zipBackupDays,
+ },
+ });
+
+ setZipBackupDays(undefined);
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
Add Notification URL
+
+ {!appriseNotification &&
No notifications stored
}
+ {appriseNotification && (
+ <>
+
+ {Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
+ return (
+ <>
+
{title}
+ {urls.map((url: string) => {
+ return (
+
+ {url}
+ {
+ await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
+
+ setRefresh(true);
+ }}
+ />
+
+ );
+ })}
+ >
+ );
+ })}
+
+ >
+ )}
+
+
+
+
+ Send notification on completed tasks with the help of the{' '}
+
+ Apprise
+ {' '}
+ library.
+
+
+
{
+ setNotificationTask(e.currentTarget.value);
+ }}
+ >
+ -- select task --
+ Rescan your Subscriptions
+ Add to download queue
+ Downloading
+ Reindex Documents
+
+
+
{
+ setNotificationUrl(e.currentTarget.value);
+ }}
+ />
+
{
+ await createAppriseNotificationUrl(
+ notificationTask as AppriseTaskNameType,
+ notificationUrl || '',
+ );
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsScheduling;
diff --git a/frontend/src/pages/SettingsUser.tsx b/frontend/src/pages/SettingsUser.tsx
index 16fbb7ba..a52235a8 100644
--- a/frontend/src/pages/SettingsUser.tsx
+++ b/frontend/src/pages/SettingsUser.tsx
@@ -1,143 +1,140 @@
-import { useLoaderData, useNavigate, useOutletContext } from 'react-router-dom';
-import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
-import { useEffect, useState } from 'react';
-import loadUserMeConfig from '../api/loader/loadUserConfig';
-import { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
-import SettingsNavigation from '../components/SettingsNavigation';
-import Notifications from '../components/Notifications';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-import { OutletContextType } from './Base';
-
-type SettingsUserLoaderData = {
- userConfig: UserMeType;
-};
-
-const SettingsUser = () => {
- const { isAdmin } = useOutletContext() as OutletContextType;
- const { userConfig } = useLoaderData() as SettingsUserLoaderData;
- const navigate = useNavigate();
-
- const userMeConfig = userConfig.config;
- const { stylesheet, page_size } = userMeConfig;
-
- const [selectedStylesheet, setSelectedStylesheet] = useState(userMeConfig.stylesheet);
- const [selectedPageSize, setSelectedPageSize] = useState(userMeConfig.page_size);
- const [refresh, setRefresh] = useState(false);
-
- const [userConfigResponse, setUserConfigResponse] = useState();
-
- const stylesheetOverwritable =
- userConfigResponse?.stylesheet || stylesheet || (ColourConstant.Dark as ColourVariants);
- const pageSizeOverwritable = userConfigResponse?.page_size || page_size || 12;
-
- useEffect(() => {
- (async () => {
- if (refresh) {
- const userConfigResponse = await loadUserMeConfig();
-
- setUserConfigResponse(userConfigResponse.config);
- setRefresh(false);
- navigate(0);
- }
- })();
- }, [navigate, refresh]);
-
- return (
- <>
-
- TA | User Settings
-
-
-
-
-
-
-
User Configurations
-
-
-
-
Stylesheet
-
-
- Current stylesheet:{' '}
- {stylesheetOverwritable}
-
-
Select your preferred stylesheet.
-
-
{
- setSelectedStylesheet(event.target.value as ColourVariants);
- }}
- >
- -- change stylesheet --
- {Object.entries(ColourConstant).map(([key, value]) => {
- return (
-
- {key}
-
- );
- })}
-
-
-
-
-
Archive View
-
-
- Current page size: {pageSizeOverwritable}
-
-
Result of videos showing in archive page
-
-
-
{
- setSelectedPageSize(Number(event.target.value));
- }}
- >
-
-
-
{
- await updateUserConfig({
- page_size: selectedPageSize,
- stylesheet: selectedStylesheet,
- });
-
- setRefresh(true);
- }}
- />
-
-
- {isAdmin && (
- <>
-
-
Users
-
-
-
User Management
-
- Access the admin interface for basic user management functionality like adding and
- deleting users, changing passwords and more.
-
-
-
-
-
- >
- )}
-
- >
- );
-};
-
-export default SettingsUser;
+import { useLoaderData, useNavigate, useOutletContext } from 'react-router-dom';
+import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
+import { useEffect, useState } from 'react';
+import loadUserMeConfig from '../api/loader/loadUserConfig';
+import { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
+import SettingsNavigation from '../components/SettingsNavigation';
+import Notifications from '../components/Notifications';
+import Button from '../components/Button';
+import { OutletContextType } from './Base';
+
+type SettingsUserLoaderData = {
+ userConfig: UserMeType;
+};
+
+const SettingsUser = () => {
+ const { isAdmin } = useOutletContext() as OutletContextType;
+ const { userConfig } = useLoaderData() as SettingsUserLoaderData;
+ const navigate = useNavigate();
+
+ const userMeConfig = userConfig.config;
+ const { stylesheet, page_size } = userMeConfig;
+
+ const [selectedStylesheet, setSelectedStylesheet] = useState(userMeConfig.stylesheet);
+ const [selectedPageSize, setSelectedPageSize] = useState(userMeConfig.page_size);
+ const [refresh, setRefresh] = useState(false);
+
+ const [userConfigResponse, setUserConfigResponse] = useState();
+
+ const stylesheetOverwritable =
+ userConfigResponse?.stylesheet || stylesheet || (ColourConstant.Dark as ColourVariants);
+ const pageSizeOverwritable = userConfigResponse?.page_size || page_size || 12;
+
+ useEffect(() => {
+ (async () => {
+ if (refresh) {
+ const userConfigResponse = await loadUserMeConfig();
+
+ setUserConfigResponse(userConfigResponse.config);
+ setRefresh(false);
+ navigate(0);
+ }
+ })();
+ }, [navigate, refresh]);
+
+ return (
+ <>
+ TA | User Settings
+
+
+
+
+
+
User Configurations
+
+
+
+
Stylesheet
+
+
+ Current stylesheet:{' '}
+ {stylesheetOverwritable}
+
+
Select your preferred stylesheet.
+
+
{
+ setSelectedStylesheet(event.target.value as ColourVariants);
+ }}
+ >
+ -- change stylesheet --
+ {Object.entries(ColourConstant).map(([key, value]) => {
+ return (
+
+ {key}
+
+ );
+ })}
+
+
+
+
+
Archive View
+
+
+ Current page size: {pageSizeOverwritable}
+
+
Result of videos showing in archive page
+
+
+
{
+ setSelectedPageSize(Number(event.target.value));
+ }}
+ >
+
+
+
{
+ await updateUserConfig({
+ page_size: selectedPageSize,
+ stylesheet: selectedStylesheet,
+ });
+
+ setRefresh(true);
+ }}
+ />
+
+
+ {isAdmin && (
+ <>
+
+
Users
+
+
+
User Management
+
+ Access the admin interface for basic user management functionality like adding and
+ deleting users, changing passwords and more.
+
+
+
+
+
+ >
+ )}
+
+ >
+ );
+};
+
+export default SettingsUser;
diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx
index 5ba480a3..5f671572 100644
--- a/frontend/src/pages/Video.tsx
+++ b/frontend/src/pages/Video.tsx
@@ -1,530 +1,604 @@
-import { Link, useNavigate, useOutletContext, useParams } from 'react-router-dom';
-import loadVideoById from '../api/loader/loadVideoById';
-import { Fragment, useEffect, useState } from 'react';
-import { ConfigType, VideoType } from './Home';
-import VideoPlayer, { VideoProgressType } from '../components/VideoPlayer';
-import iconEye from '/img/icon-eye.svg';
-import iconThumb from '/img/icon-thumb.svg';
-import iconStarFull from '/img/icon-star-full.svg';
-import iconStarEmpty from '/img/icon-star-empty.svg';
-import iconStarHalf from '/img/icon-star-half.svg';
-import iconClose from '/img/icon-close.svg';
-import iconUnseen from '/img/icon-unseen.svg';
-import iconSeen from '/img/icon-seen.svg';
-import Routes from '../configuration/routes/RouteList';
-import Linkify from '../components/Linkify';
-import loadSimmilarVideosById from '../api/loader/loadSimmilarVideosById';
-import VideoList from '../components/VideoList';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import humanFileSize from '../functions/humanFileSize';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import loadVideoProgressById from '../api/loader/loadVideoProgressById';
-import ChannelOverview from '../components/ChannelOverview';
-import deleteVideo from '../api/actions/deleteVideo';
-import capitalizeFirstLetter from '../functions/capitalizeFirstLetter';
-import formatDate from '../functions/formatDates';
-import formatNumbers from '../functions/formatNumbers';
-import queueReindex from '../api/actions/queueReindex';
-import GoogleCast from '../components/GoogleCast';
-import WatchedCheckBox from '../components/WatchedCheckBox';
-import convertStarRating from '../functions/convertStarRating';
-import loadPlaylistList from '../api/loader/loadPlaylistList';
-import { PlaylistsResponseType } from './Playlists';
-import PaginationDummy from '../components/PaginationDummy';
-import updateCustomPlaylist from '../api/actions/updateCustomPlaylist';
-import { PlaylistType } from './Playlist';
-import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
-import CommentBox, { CommentsType } from '../components/CommentBox';
-import { Helmet } from 'react-helmet';
-import Button from '../components/Button';
-import { OutletContextType } from './Base';
-import getApiUrl from '../configuration/getApiUrl';
-import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav';
-
-const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
- return playlist.playlist_entries.some(entry => {
- return entry.youtube_id === videoId;
- });
-};
-
-type VideoParams = {
- videoId: string;
-};
-
-type PlaylistNavPreviousItemType = {
- youtube_id: string;
- vid_thumb: string;
- idx: number;
- title: string;
-};
-
-type PlaylistNavNextItemType = {
- youtube_id: string;
- vid_thumb: string;
- idx: number;
- title: string;
-};
-
-type PlaylistNavItemType = {
- playlist_meta: {
- current_idx: string;
- playlist_id: string;
- playlist_name: string;
- playlist_channel: string;
- };
- playlist_previous: PlaylistNavPreviousItemType;
- playlist_next: PlaylistNavNextItemType;
-};
-
-type PlaylistNavType = PlaylistNavItemType[];
-
-export type SponsorBlockSegmentType = {
- category: string;
- actionType: string;
- segment: number[];
- UUID: string;
- videoDuration: number;
- locked: number;
- votes: number;
-};
-
-export type SponsorBlockType = {
- last_refresh: number;
- has_unlocked: boolean;
- is_enabled: boolean;
- segments: SponsorBlockSegmentType[];
- message?: string;
-};
-
-export type SimilarVideosResponseType = {
- data: VideoType[];
- config: ConfigType;
-};
-
-export type VideoResponseType = {
- data: VideoType;
- config: ConfigType;
-};
-
-type CommentsResponseType = {
- data: CommentsType[];
- config: ConfigType;
-};
-
-export type VideoCommentsResponseType = {
- data: VideoType;
- config: ConfigType;
- playlist_nav: PlaylistNavType;
-};
-
-const Video = () => {
- const { isAdmin } = useOutletContext() as OutletContextType;
- const { videoId } = useParams() as VideoParams;
- const navigate = useNavigate();
-
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [showAddToPlaylist, setShowAddToPlaylist] = useState(false);
- const [refreshVideoList, setRefreshVideoList] = useState(false);
- const [reindex, setReindex] = useState(false);
-
- const [videoResponse, setVideoResponse] = useState();
- const [simmilarVideos, setSimmilarVideos] = useState();
- const [videoProgress, setVideoProgress] = useState();
- const [videoPlaylistNav, setVideoPlaylistNav] = useState();
- const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState();
- const [commentsResponse, setCommentsResponse] = useState();
-
- useEffect(() => {
- (async () => {
- const videoResponse = await loadVideoById(videoId);
- const simmilarVideosResponse = await loadSimmilarVideosById(videoId);
- const videoProgressResponse = await loadVideoProgressById(videoId);
- const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' });
- const commentsResponse = await loadCommentsbyVideoId(videoId);
- const videoNavResponse = await loadVideoNav(videoId);
-
- setVideoResponse(videoResponse);
- setSimmilarVideos(simmilarVideosResponse);
- setVideoProgress(videoProgressResponse);
- setVideoPlaylistNav(videoNavResponse);
- setCustomPlaylistsResponse(customPlaylistsResponse);
- setCommentsResponse(commentsResponse);
- setRefreshVideoList(false);
- })();
- }, [videoId, refreshVideoList]);
-
- if (videoResponse === undefined) {
- return [];
- }
-
- const video = videoResponse.data;
- const watched = videoResponse.data.player.watched;
- const config = videoResponse.config;
- const playlistNav = videoPlaylistNav;
- const sponsorBlock = videoResponse.data.sponsorblock;
- const customPlaylists = customPlaylistsResponse?.data;
- const starRating = convertStarRating(video?.stats?.average_rating);
- const comments = commentsResponse?.data;
-
- console.log('playlistNav', playlistNav);
-
- const cast = config.enable_cast;
-
- return (
- <>
-
- TA | {video.title}
-
-
-
-
-
-
-
- {cast && (
- {
- setRefreshVideoList(true);
- }}
- />
- )}
- {video.title}
-
-
-
-
-
-
-
Published: {formatDate(video.published)}
-
Last refreshed: {formatDate(video.vid_last_refresh)}
-
- Watched:
- {
- await updateWatchedState({
- id: videoId,
- is_watched: status,
- });
-
- setRefreshVideoList(true);
- }}
- />
-
- {video.active && (
-
- Youtube:{' '}
-
- Active
-
-
- )}
- {!video.active &&
Youtube: Deactivated
}
-
-
-
-
-
- : {formatNumbers(video.stats.view_count)}
-
-
- : {formatNumbers(video.stats.like_count)}
-
- {video.stats.dislike_count > 0 && (
-
- :{' '}
- {video.stats.dislike_count}
-
- )}
- {video?.stats && starRating && (
-
- {starRating?.map?.((star, index) => {
- if (star === 'full') {
- return
;
- }
-
- if (star === 'half') {
- return
;
- }
-
- return
;
- })}
-
- )}
-
-
-
-
-
-
- {reindex &&
Reindex scheduled
}
- {!reindex && (
- <>
- {isAdmin && (
-
- {
- await queueReindex(video.youtube_id, 'video');
- setReindex(true);
- }}
- />
-
- )}
- >
- )}
-
-
- {' '}
- {isAdmin && (
- <>
- {!showDeleteConfirm && (
-
setShowDeleteConfirm(!showDeleteConfirm)}
- />
- )}
-
- {showDeleteConfirm && (
-
- Are you sure?
- {
- await deleteVideo(videoId);
- navigate(Routes.Channel(video.channel.channel_id));
- }}
- />{' '}
- setShowDeleteConfirm(!showDeleteConfirm)}
- />
-
- )}
- >
- )}{' '}
- {!showAddToPlaylist && (
- {
- setShowAddToPlaylist(true);
- }}
- />
- )}
- {showAddToPlaylist && (
- <>
-
-
{
- setShowAddToPlaylist(false);
- }}
- />
-
Add video to...
-
- {customPlaylists?.map(playlist => {
- return (
-
{
- if (isInPlaylist(videoId, playlist)) {
- await updateCustomPlaylist('remove', playlist.playlist_id, videoId);
- } else {
- await updateCustomPlaylist('create', playlist.playlist_id, videoId);
- }
-
- setRefreshVideoList(true);
- }}
- >
- {isInPlaylist(videoId, playlist) && (
-
- )}
-
- {!isInPlaylist(videoId, playlist) && (
-
- )}
-
- {playlist.playlist_name}
-
- );
- })}
-
-
- Create playlist
-
-
- >
- )}
-
-
-
- {video.media_size &&
File size: {humanFileSize(video.media_size)}
}
-
- {video.streams &&
- video.streams.map(stream => {
- return (
-
- {capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
- {humanFileSize(stream.bitrate)}/s
- {stream.width && (
- <>
- | {stream.width}x{stream.height}
- >
- )}{' '}
-
- );
- })}
-
-
- {video.tags && video.tags.length > 0 && (
-
-
- {video.tags.map(tag => {
- return (
-
- {tag}
-
- );
- })}
-
-
- )}
-
- {video.description && (
-
-
- {video.description}
-
-
-
setDescriptionExpanded(!descriptionExpanded)}
- />
-
- )}
-
- {playlistNav && (
- <>
- {playlistNav.map(playlistItem => {
- return (
-
-
-
- Playlist [{playlistItem.playlist_meta.current_idx + 1}
- ]: {playlistItem.playlist_meta.playlist_name}
-
-
-
-
- {playlistItem.playlist_previous && (
- <>
-
-
-
-
-
Previous:
-
-
- [{playlistItem.playlist_previous.idx + 1}]{' '}
- {playlistItem.playlist_previous.title}
-
-
-
- >
- )}
-
-
- {playlistItem.playlist_next && (
- <>
-
-
Next:
-
-
- [{playlistItem.playlist_next.idx + 1}]{' '}
- {playlistItem.playlist_next.title}
-
-
-
-
-
-
- >
- )}
-
-
-
- );
- })}
- >
- )}
-
-
-
- {video.comment_count == 0 && (
-
- Video has no comments
-
- )}
-
- {video.comment_count && (
-
-
Comments: {video.comment_count}
-
-
- )}
-
-
-
-
- >
- );
-};
-
-export default Video;
+import { Link, useNavigate, useOutletContext, useParams } from 'react-router-dom';
+import loadVideoById from '../api/loader/loadVideoById';
+import { Fragment, useEffect, useState } from 'react';
+import { ConfigType, VideoType } from './Home';
+import VideoPlayer, { VideoProgressType } from '../components/VideoPlayer';
+import iconEye from '/img/icon-eye.svg';
+import iconThumb from '/img/icon-thumb.svg';
+import iconStarFull from '/img/icon-star-full.svg';
+import iconStarEmpty from '/img/icon-star-empty.svg';
+import iconStarHalf from '/img/icon-star-half.svg';
+import iconClose from '/img/icon-close.svg';
+import iconUnseen from '/img/icon-unseen.svg';
+import iconSeen from '/img/icon-seen.svg';
+import Routes from '../configuration/routes/RouteList';
+import Linkify from '../components/Linkify';
+import loadSimmilarVideosById from '../api/loader/loadSimmilarVideosById';
+import VideoList from '../components/VideoList';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import humanFileSize from '../functions/humanFileSize';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import loadVideoProgressById from '../api/loader/loadVideoProgressById';
+import ChannelOverview from '../components/ChannelOverview';
+import deleteVideo from '../api/actions/deleteVideo';
+import capitalizeFirstLetter from '../functions/capitalizeFirstLetter';
+import formatDate from '../functions/formatDates';
+import formatNumbers from '../functions/formatNumbers';
+import queueReindex from '../api/actions/queueReindex';
+import GoogleCast from '../components/GoogleCast';
+import WatchedCheckBox from '../components/WatchedCheckBox';
+import convertStarRating from '../functions/convertStarRating';
+import loadPlaylistList from '../api/loader/loadPlaylistList';
+import { PlaylistsResponseType } from './Playlists';
+import PaginationDummy from '../components/PaginationDummy';
+import updateCustomPlaylist from '../api/actions/updateCustomPlaylist';
+import { PlaylistType } from './Playlist';
+import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
+import CommentBox, { CommentsType } from '../components/CommentBox';
+import Button from '../components/Button';
+import { OutletContextType } from './Base';
+import getApiUrl from '../configuration/getApiUrl';
+import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav';
+
+const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
+ return playlist.playlist_entries.some(entry => {
+ return entry.youtube_id === videoId;
+ });
+};
+
+type VideoParams = {
+ videoId: string;
+};
+
+type PlaylistNavPreviousItemType = {
+ youtube_id: string;
+ vid_thumb: string;
+ idx: number;
+ title: string;
+};
+
+type PlaylistNavNextItemType = {
+ youtube_id: string;
+ vid_thumb: string;
+ idx: number;
+ title: string;
+};
+
+type PlaylistNavItemType = {
+ playlist_meta: {
+ current_idx: string;
+ playlist_id: string;
+ playlist_name: string;
+ playlist_channel: string;
+ };
+ playlist_previous: PlaylistNavPreviousItemType;
+ playlist_next: PlaylistNavNextItemType;
+};
+
+type PlaylistNavType = PlaylistNavItemType[];
+
+export type SponsorBlockSegmentType = {
+ category: string;
+ actionType: string;
+ segment: number[];
+ UUID: string;
+ videoDuration: number;
+ locked: number;
+ votes: number;
+};
+
+export type SponsorBlockType = {
+ last_refresh: number;
+ has_unlocked: boolean;
+ is_enabled: boolean;
+ segments: SponsorBlockSegmentType[];
+ message?: string;
+};
+
+export type SimilarVideosResponseType = {
+ data: VideoType[];
+ config: ConfigType;
+};
+
+export type VideoResponseType = {
+ data: VideoType;
+ config: ConfigType;
+};
+
+type CommentsResponseType = {
+ data: CommentsType[];
+ config: ConfigType;
+};
+
+export type VideoCommentsResponseType = {
+ data: VideoType;
+ config: ConfigType;
+ playlist_nav: PlaylistNavType;
+};
+
+const Video = () => {
+ const { isAdmin } = useOutletContext() as OutletContextType;
+ const { videoId } = useParams() as VideoParams;
+ const navigate = useNavigate();
+
+ const [loading, setLoading] = useState(false);
+ const [videoEnded, setVideoEnded] = useState(false);
+ const [playlistAutoplay, setPlaylistAutoplay] = useState(
+ localStorage.getItem('playlistAutoplay') === 'true',
+ );
+ const [playlistIdForAutoplay, setPlaylistIDForAutoplay] = useState(
+ localStorage.getItem('playlistIdForAutoplay') ?? '',
+ );
+ const [descriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [showAddToPlaylist, setShowAddToPlaylist] = useState(false);
+ const [refreshVideoList, setRefreshVideoList] = useState(false);
+ const [reindex, setReindex] = useState(false);
+
+ const [videoResponse, setVideoResponse] = useState();
+ const [simmilarVideos, setSimmilarVideos] = useState();
+ const [videoProgress, setVideoProgress] = useState();
+ const [videoPlaylistNav, setVideoPlaylistNav] = useState();
+ const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState();
+ const [commentsResponse, setCommentsResponse] = useState();
+
+ useEffect(() => {
+ (async () => {
+ setLoading(true);
+
+ const videoResponse = await loadVideoById(videoId);
+ const simmilarVideosResponse = await loadSimmilarVideosById(videoId);
+ const videoProgressResponse = await loadVideoProgressById(videoId);
+ const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' });
+ const commentsResponse = await loadCommentsbyVideoId(videoId);
+ const videoNavResponse = await loadVideoNav(videoId);
+
+ setVideoResponse(videoResponse);
+ setSimmilarVideos(simmilarVideosResponse);
+ setVideoProgress(videoProgressResponse);
+ setVideoPlaylistNav(videoNavResponse);
+ setCustomPlaylistsResponse(customPlaylistsResponse);
+ setCommentsResponse(commentsResponse);
+ setRefreshVideoList(false);
+
+ setLoading(false);
+ })();
+ }, [videoId, refreshVideoList]);
+
+ useEffect(() => {
+ localStorage.setItem('playlistAutoplay', playlistAutoplay.toString());
+
+ if (!playlistAutoplay) {
+ localStorage.setItem('playlistIdForAutoplay', '');
+
+ return;
+ }
+
+ localStorage.setItem('playlistIdForAutoplay', playlistIdForAutoplay || '');
+ }, [playlistAutoplay, playlistIdForAutoplay]);
+
+ useEffect(() => {
+ if (videoEnded && playlistAutoplay) {
+ const playlist = videoPlaylistNav?.find(playlist => {
+ return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
+ });
+
+ if (playlist) {
+ const nextYoutubeId = playlist.playlist_next?.youtube_id;
+
+ if (nextYoutubeId) {
+ setVideoEnded(false);
+ navigate(Routes.Video(nextYoutubeId));
+ }
+ }
+ }
+ }, [videoEnded, playlistAutoplay]);
+
+ if (videoResponse === undefined) {
+ return [];
+ }
+
+ const video = videoResponse.data;
+ const watched = videoResponse.data.player.watched;
+ const config = videoResponse.config;
+ const playlistNav = videoPlaylistNav;
+ const sponsorBlock = videoResponse.data.sponsorblock;
+ const customPlaylists = customPlaylistsResponse?.data;
+ const starRating = convertStarRating(video?.stats?.average_rating);
+ const comments = commentsResponse?.data;
+
+ console.log('playlistNav', playlistNav);
+
+ const cast = config.enable_cast;
+
+ return (
+ <>
+ {`TA | ${video.title}`}
+
+
+ {!loading && (
+ {
+ setVideoEnded(true);
+ }}
+ />
+ )}
+
+
+
+ {cast && (
+ {
+ setRefreshVideoList(true);
+ }}
+ />
+ )}
+ {video.title}
+
+
+
+
+
+
+
Published: {formatDate(video.published)}
+
Last refreshed: {formatDate(video.vid_last_refresh)}
+
+ Watched:
+ {
+ await updateWatchedState({
+ id: videoId,
+ is_watched: status,
+ });
+ }}
+ onDone={() => {
+ setRefreshVideoList(true);
+ }}
+ />
+
+ {video.active && (
+
+ Youtube:{' '}
+
+ Active
+
+
+ )}
+ {!video.active &&
Youtube: Deactivated
}
+
+
+
+
+
+ : {formatNumbers(video.stats.view_count)}
+
+
+ : {formatNumbers(video.stats.like_count)}
+
+ {video.stats.dislike_count > 0 && (
+
+ :{' '}
+ {video.stats.dislike_count}
+
+ )}
+ {video?.stats && starRating && (
+
+ {starRating?.map?.((star, index) => {
+ if (star === 'full') {
+ return
;
+ }
+
+ if (star === 'half') {
+ return
;
+ }
+
+ return
;
+ })}
+
+ )}
+
+
+
+
+
+
+ {reindex &&
Reindex scheduled
}
+ {!reindex && (
+ <>
+ {isAdmin && (
+
+ {
+ await queueReindex(video.youtube_id, 'video');
+ setReindex(true);
+ }}
+ />
+
+ )}
+ >
+ )}
+
+
+ {' '}
+ {isAdmin && (
+ <>
+ {!showDeleteConfirm && (
+
setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+ )}
+
+ {showDeleteConfirm && (
+
+ Are you sure?
+ {
+ await deleteVideo(videoId);
+ navigate(Routes.Channel(video.channel.channel_id));
+ }}
+ />{' '}
+ setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+
+ )}
+ >
+ )}{' '}
+ {!showAddToPlaylist && (
+ {
+ setShowAddToPlaylist(true);
+ }}
+ />
+ )}
+ {showAddToPlaylist && (
+ <>
+
+
{
+ setShowAddToPlaylist(false);
+ }}
+ />
+
Add video to...
+
+ {customPlaylists?.map(playlist => {
+ return (
+
{
+ if (isInPlaylist(videoId, playlist)) {
+ await updateCustomPlaylist('remove', playlist.playlist_id, videoId);
+ } else {
+ await updateCustomPlaylist('create', playlist.playlist_id, videoId);
+ }
+
+ setRefreshVideoList(true);
+ }}
+ >
+ {isInPlaylist(videoId, playlist) && (
+
+ )}
+
+ {!isInPlaylist(videoId, playlist) && (
+
+ )}
+
+ {playlist.playlist_name}
+
+ );
+ })}
+
+
+ Create playlist
+
+
+ >
+ )}
+
+
+
+ {video.media_size &&
File size: {humanFileSize(video.media_size)}
}
+
+ {video.streams &&
+ video.streams.map(stream => {
+ return (
+
+ {capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
+ {humanFileSize(stream.bitrate)}/s
+ {stream.width && (
+ <>
+ | {stream.width}x{stream.height}
+ >
+ )}{' '}
+
+ );
+ })}
+
+
+ {video.tags && video.tags.length > 0 && (
+
+
+ {video.tags.map(tag => {
+ return (
+
+ {tag}
+
+ );
+ })}
+
+
+ )}
+
+ {video.description && (
+
+
+ {video.description}
+
+
+
setDescriptionExpanded(!descriptionExpanded)}
+ />
+
+ )}
+
+ {playlistNav && (
+ <>
+ {playlistNav.map(playlistItem => {
+ return (
+
+
+
+ Playlist [{playlistItem.playlist_meta.current_idx + 1}
+ ]: {playlistItem.playlist_meta.playlist_name}
+
+
+
+
+
Autoplay:
+
+ {
+ if (!playlistAutoplay) {
+ setPlaylistIDForAutoplay(playlistItem.playlist_meta.playlist_id);
+ }
+
+ setPlaylistAutoplay(!playlistAutoplay);
+ }}
+ type="checkbox"
+ />
+ {!playlistAutoplay && (
+
+ Off
+
+ )}
+ {playlistAutoplay && (
+
+ On
+
+ )}
+
+
+
+
+
+ {playlistItem.playlist_previous && (
+ <>
+
+
+
+
+
Previous:
+
+
+ [{playlistItem.playlist_previous.idx + 1}]{' '}
+ {playlistItem.playlist_previous.title}
+
+
+
+ >
+ )}
+
+
+ {playlistItem.playlist_next && (
+ <>
+
+
Next:
+
+
+ [{playlistItem.playlist_next.idx + 1}]{' '}
+ {playlistItem.playlist_next.title}
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ );
+ })}
+ >
+ )}
+
+
+
+ {video.comment_count == 0 && (
+
+ Video has no comments
+
+ )}
+
+ {video.comment_count && (
+
+
Comments: {video.comment_count}
+
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Video;
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 022de910..1621d5ec 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -1361,3 +1361,45 @@ video:-webkit-full-screen {
display: flex;
justify-content: space-between;
}
+
+/** loading indicator */
+/** source: https://github.com/loadingio/css-spinner/ */
+.lds-ring,
+.lds-ring div {
+ box-sizing: border-box;
+}
+.lds-ring {
+ display: inline-block;
+ position: relative;
+ width: 20px;
+ height: 20px;
+ margin-right: 10px;
+}
+.lds-ring div {
+ box-sizing: border-box;
+ display: block;
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border: 4px solid currentColor;
+ border-radius: 50%;
+ animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+ border-color: currentColor transparent transparent transparent;
+}
+.lds-ring div:nth-child(1) {
+ animation-delay: -0.45s;
+}
+.lds-ring div:nth-child(2) {
+ animation-delay: -0.3s;
+}
+.lds-ring div:nth-child(3) {
+ animation-delay: -0.15s;
+}
+@keyframes lds-ring {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 2ed1ecc5..b4e1e072 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
- "target": "ES2022",
+ "target": "ESNext",
"useDefineForClassFields": true,
- "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,