commit 194fe675c807661089bd7baaca72504b64288ce4 Author: mboehmlaender Date: Wed Feb 25 10:40:38 2026 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f4c8b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +__pycache__/ +*.pyc +data/pyload_settings.json +data/pyload_secret.key diff --git a/app.py b/app.py new file mode 100644 index 0000000..5cd73a0 --- /dev/null +++ b/app.py @@ -0,0 +1,513 @@ +import json +import re +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit +from flask import Flask, jsonify, request, send_from_directory +from cryptography.fernet import Fernet, InvalidToken +import requests + +app = Flask(__name__, static_folder="dist", static_url_path="") + +API = "https://mediathekviewweb.de/api/query" +CHANNELS_API = "https://mediathekviewweb.de/api/channels" +DIST_DIR = Path(app.root_path) / "dist" +DATA_DIR = Path(app.root_path) / "data" +PYLOAD_SETTINGS_FILE = DATA_DIR / "pyload_settings.json" +PYLOAD_KEY_FILE = DATA_DIR / "pyload_secret.key" +DEFAULT_RESULTS_PER_PAGE = 8 +VIDEO_SIZE_CACHE = {} +CONTENT_RANGE_SIZE_RE = re.compile(r"/(\d+)$") + + +def fetch_channels(): + try: + response = requests.get(CHANNELS_API, timeout=30) + response.raise_for_status() + data = response.json() + channels = data.get("channels") if isinstance(data, dict) else None + + if isinstance(channels, list): + clean_channels = sorted({ + str(channel).strip() + for channel in channels + if str(channel).strip() + }) + if clean_channels: + return clean_channels + except (requests.RequestException, ValueError): + pass + + # Fallback for compatibility in case /api/channels is temporarily unavailable. + payload = {"queries": [], "size": 5000} + response = requests.post(API, json=payload, timeout=30) + response.raise_for_status() + + return sorted({ + e.get("channel") + for e in response.json()["result"]["results"] + if e.get("channel") + }) + + +def search(term, channels): + offset = 0 + size = 50 + results = [] + + queries = [{"fields": ["title", "topic"], "query": term}] + + for ch in channels: + queries.append({"fields": ["channel"], "query": ch}) + + while True: + payload = { + "queries": queries, + "offset": offset, + "size": size + } + + r = requests.post(API, json=payload, timeout=30) + r.raise_for_status() + data = r.json()["result"]["results"] + + if not data: + break + + for e in data: + title = e.get("title") or "" + if "originalversion" in title.lower(): + continue + + qualities = [] + seen_urls = set() + quality_candidates = [ + ("hd", "HD", e.get("url_video_hd"), e.get("size_hd")), + ("standard", "Standard", e.get("url_video"), e.get("size")), + ("low", "Niedrig", e.get("url_video_low"), e.get("size_low")), + ] + for qid, label, url, quality_size in quality_candidates: + if not url: + continue + clean_url = str(url).strip() + if not clean_url or clean_url in seen_urls: + continue + seen_urls.add(clean_url) + qualities.append({ + "id": qid, + "label": label, + "url": clean_url, + "size": int(quality_size) if isinstance(quality_size, (int, float)) else None, + }) + + default_quality = qualities[0]["id"] if qualities else "" + + results.append({ + "id": f"{title}_{e.get('timestamp')}", + "title": title, + "topic": e.get("topic"), + "channel": e.get("channel"), + "timestamp": e.get("timestamp"), + "duration": e.get("duration"), + "size": e.get("size"), + "description": e.get("description"), + "qualities": qualities, + "default_quality": default_quality, + "subtitle": e.get("url_subtitle"), + "url": e.get("url_website") + }) + + offset += size + + return results + + +def parse_positive_int(value): + if value is None: + return None + + try: + parsed = int(value) + except (TypeError, ValueError): + return None + + if parsed <= 0: + return None + + return parsed + + +def size_from_response_headers(response): + content_length = parse_positive_int(response.headers.get("Content-Length")) + if content_length: + return content_length + + content_range = (response.headers.get("Content-Range") or "").strip() + if not content_range: + return None + + match = CONTENT_RANGE_SIZE_RE.search(content_range) + if not match: + return None + + return parse_positive_int(match.group(1)) + + +def resolve_video_size(url): + value = str(url or "").strip() + if not value: + return None + + if value in VIDEO_SIZE_CACHE: + return VIDEO_SIZE_CACHE[value] + + size = None + + try: + response = requests.head(value, allow_redirects=True, timeout=12) + if response.ok: + size = size_from_response_headers(response) + except requests.RequestException: + pass + + if size is None: + try: + response = requests.get( + value, + allow_redirects=True, + timeout=15, + stream=True, + headers={"Range": "bytes=0-0"}, + ) + if response.ok or response.status_code == 206: + size = size_from_response_headers(response) + except requests.RequestException: + pass + + VIDEO_SIZE_CACHE[value] = size + return size + + +def normalize_pyload_api_base(server): + value = (server or "").strip() + if not value: + raise ValueError("pyLoad-Server fehlt.") + + if not value.startswith(("http://", "https://")): + value = f"http://{value}" + + parts = urlsplit(value) + if not parts.netloc: + raise ValueError("pyLoad-Server ist ungueltig.") + + path = parts.path.rstrip("/") + if not path: + path = "/api" + elif not path.endswith("/api"): + path = f"{path}/api" + + return urlunsplit((parts.scheme, parts.netloc, path, "", "")) + + +def response_data(response): + try: + return response.json() + except ValueError: + return response.text.strip() + + +def request_error_message(exc): + if getattr(exc, "response", None) is None: + return str(exc) + + response = exc.response + text = (response.text or "").strip().replace("\n", " ") + if len(text) > 220: + text = f"{text[:220]}..." + + if text: + return f"HTTP {response.status_code}: {text}" + + return f"HTTP {response.status_code}" + + +def ensure_data_dir(): + DATA_DIR.mkdir(parents=True, exist_ok=True) + + +def get_cipher(): + ensure_data_dir() + + if PYLOAD_KEY_FILE.exists(): + key = PYLOAD_KEY_FILE.read_bytes().strip() + else: + key = Fernet.generate_key() + PYLOAD_KEY_FILE.write_bytes(key) + try: + PYLOAD_KEY_FILE.chmod(0o600) + except OSError: + pass + + return Fernet(key) + + +def encrypt_password(password): + if not password: + return "" + + return get_cipher().encrypt(password.encode("utf-8")).decode("utf-8") + + +def decrypt_password(token): + if not token: + return "" + + try: + return get_cipher().decrypt(token.encode("utf-8")).decode("utf-8") + except (InvalidToken, ValueError): + return "" + + +def load_pyload_settings(): + if not PYLOAD_SETTINGS_FILE.exists(): + return { + "server": "", + "username": "", + "password": "", + "results_per_page": DEFAULT_RESULTS_PER_PAGE, + } + + try: + content = json.loads(PYLOAD_SETTINGS_FILE.read_text(encoding="utf-8")) + except (OSError, ValueError): + return { + "server": "", + "username": "", + "password": "", + "results_per_page": DEFAULT_RESULTS_PER_PAGE, + } + + encrypted_password = content.get("password_encrypted") or "" + results_per_page = content.get("results_per_page", DEFAULT_RESULTS_PER_PAGE) + try: + results_per_page = int(results_per_page) + except (TypeError, ValueError): + results_per_page = DEFAULT_RESULTS_PER_PAGE + if results_per_page < 1 or results_per_page > 200: + results_per_page = DEFAULT_RESULTS_PER_PAGE + + return { + "server": str(content.get("server") or "").strip(), + "username": str(content.get("username") or "").strip(), + "password": decrypt_password(str(encrypted_password)), + "results_per_page": results_per_page, + } + + +def save_pyload_settings(server, username, password, results_per_page): + try: + rows = int(results_per_page) + except (TypeError, ValueError): + rows = DEFAULT_RESULTS_PER_PAGE + if rows < 1 or rows > 200: + rows = DEFAULT_RESULTS_PER_PAGE + + ensure_data_dir() + data = { + "server": (server or "").strip(), + "username": (username or "").strip(), + "password_encrypted": encrypt_password(password or ""), + "results_per_page": rows, + } + PYLOAD_SETTINGS_FILE.write_text( + json.dumps(data, ensure_ascii=True, indent=2), + encoding="utf-8", + ) + try: + PYLOAD_SETTINGS_FILE.chmod(0o600) + except OSError: + pass + + +def pyload_add_package(session, api_base, username, password, name, links): + payload = { + "name": name, + "links": links, + "dest": 1, + } + response = session.post( + f"{api_base}/add_package", + json=payload, + auth=(username, password), + timeout=30, + ) + response.raise_for_status() + return response_data(response) + + +@app.route("/channels") +def channels(): + return jsonify(fetch_channels()) + + +@app.route("/search") +def do_search(): + term = request.args.get("q", "") + channels = request.args.getlist("channels[]") + return jsonify(search(term, channels)) + + +@app.route("/quality-sizes", methods=["POST"]) +def quality_sizes(): + body = request.get_json(silent=True) or {} + urls = body.get("urls") + + if not isinstance(urls, list): + return jsonify({"error": "urls muss ein Array sein."}), 400 + + resolved = {} + for raw_url in urls[:18]: + url = str(raw_url or "").strip() + if not url: + continue + + size = resolve_video_size(url) + if isinstance(size, int) and size > 0: + resolved[url] = size + + return jsonify({"sizes": resolved}) + + +@app.route("/pyload/settings", methods=["GET"]) +def get_pyload_settings(): + return jsonify(load_pyload_settings()) + + +@app.route("/pyload/settings", methods=["POST"]) +def set_pyload_settings(): + body = request.get_json(silent=True) or {} + + existing = load_pyload_settings() + + if "server" in body: + server = str(body.get("server") or "") + else: + server = existing.get("server") or "" + + if "username" in body: + username = str(body.get("username") or "") + else: + username = existing.get("username") or "" + + if "password" in body: + password = str(body.get("password") or "") + else: + password = existing.get("password") or "" + + if "results_per_page" in body: + results_per_page = body.get("results_per_page") + else: + results_per_page = existing.get("results_per_page", DEFAULT_RESULTS_PER_PAGE) + + save_pyload_settings(server, username, password, results_per_page) + return jsonify({"saved": True}) + + +@app.route("/pyload/add", methods=["POST"]) +def pyload_add(): + body = request.get_json(silent=True) or {} + + stored = load_pyload_settings() + server = str(body.get("server") or stored.get("server") or "").strip() + username = str(body.get("username") or stored.get("username") or "").strip() + password = body.get("password") + if password is None or password == "": + password = stored.get("password") or "" + else: + password = str(password) + packages = body.get("packages") + + if not server: + return jsonify({"error": "pyLoad-Server fehlt."}), 400 + if not username: + return jsonify({"error": "pyLoad-Benutzer fehlt."}), 400 + if not password: + return jsonify({"error": "pyLoad-Passwort fehlt."}), 400 + if not isinstance(packages, list) or not packages: + return jsonify({"error": "Keine Pakete zum Hinzufuegen."}), 400 + + clean_packages = [] + for package in packages: + if not isinstance(package, dict): + continue + + name = (package.get("name") or "").strip() + links = package.get("links") or [] + if not isinstance(links, list): + continue + + clean_links = [str(link).strip() for link in links if str(link).strip()] + if name and clean_links: + clean_packages.append({"name": name, "links": clean_links}) + + if not clean_packages: + return jsonify({"error": "Keine gueltigen Pakete enthalten."}), 400 + + try: + api_base = normalize_pyload_api_base(server) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + + session = requests.Session() + added = [] + failed = [] + + for package in clean_packages: + try: + package_id = pyload_add_package( + session, + api_base, + username, + password, + package["name"], + package["links"], + ) + added.append({"name": package["name"], "pid": package_id}) + except requests.RequestException as exc: + message = request_error_message(exc) + response = getattr(exc, "response", None) + if response is not None and response.status_code == 404: + body = (response.text or "").strip() + if "obsolete api" in body.lower(): + message = ( + "HTTP 404: Obsolete API. Bitte pyLoad REST API mit " + "Basic Auth verwenden." + ) + failed.append({ + "name": package["name"], + "error": message, + }) + + status_code = 200 if added else 502 + return jsonify({ + "added": added, + "failed": failed, + "added_count": len(added), + "failed_count": len(failed), + }), status_code + + +@app.route("/", defaults={"path": ""}) +@app.route("/") +def serve_frontend(path): + if not DIST_DIR.exists(): + return "Frontend not built. Run 'npm run build'.", 503 + + requested_file = DIST_DIR / path + if path and requested_file.exists() and requested_file.is_file(): + return send_from_directory(DIST_DIR, path) + + return send_from_directory(DIST_DIR, "index.html") + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7df8f21 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Klangkiste Control Panel + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..7795a15 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2860 @@ +{ + "name": "klangkiste-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "klangkiste-frontend", + "version": "0.1.0", + "dependencies": { + "@phosphor-icons/react": "2.1.7", + "primeicons": "^7.0.0", + "primereact": "^10.7.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "4.3.1", + "vite": "5.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz", + "integrity": "sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha512-KbiZEa9/vofNcVJXGwdWWn25reQ3V3dHBWbS07FTF3/TOehLnm9GEhJV4T6ZvGPkShRpmUqYwnaCrkj0mRnP6Q==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha512-FFN5KwpvvQTTS5hWPxrU8/QE4kQUc6uwZcrnlMBN82t1MgAtq8mnoDwINBly9Tdr02seeIIhtdF+UH1feBYGog==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha512-X8xbmTc1cbPXcQV4WkLcRMALuyoxhfpFATmyuCxJPOAvrDS4DNnsTAOmKUxMTOWU6TzrTOkxPKwIx5ZOpJVSrg==", + "deprecated": "This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + }, + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha512-V6Yw1rIcYV/4JsnggjBU0l4Kr+EXhpwqXRusENU1Xx6ro00IHPHYNynCuBTOZAPlr3AAmLvchH9I7N/VUdvOwQ==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT", + "optional": 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==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)", + "optional": true + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=0.12" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.2.11", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "2.81.0", + "source-map": "^0.5.3" + } + }, + "node_modules/less/node_modules/ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "node_modules/less/node_modules/assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha512-u1L0ZLywRziOVjUhRxI0Qg9G+4RnFB9H/Rq40YWn0dieDgO7vAYeJz6jKAO6t/aruzlDFLAPkQTT87e+f8Imaw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/less/node_modules/aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha512-JnJpAS0p9RmixkOvW2XwDxxzs1bd4/VAGIl6Q0EC5YOo+p+hqIhtDhn/nmFnB/xUNXbLkpE2mOjgVIBRKD4xYw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/less/node_modules/form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha512-8HWGSLAPr+AG0hBpsqi5Ob8HrLStN/LWeqhpFl14d7FJgHK48TmgLoALPz69XSUR65YJzDfLUX/BM8+MLJLghQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/less/node_modules/har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha512-f8xf2GOR6Rgwc9FPTLNzgwB+JQ2/zMauYXSWmX5YV5acex6VomT0ocSuwR7BfXo5MpHi+jL+saaux2fwsGJDKQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha512-5Gbp6RAftMYYV3UEI4c4Vv3+a4dQ7taVyvHt+/L6kRt+f4HX1GweAk5UDWN0SvdVnRBzGQ6OG89pGaD9uSFnVw==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha512-iUn0NcRULlDGtqNLN1Jxmzayk8ogm7NToldASyZBpM2qggbphjXzNOiw3piN8tgz+e/DRs6X5gAzFwTI6BCRcg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/less/node_modules/oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha512-VlF07iu3VV3+BTXj43Nmp6Irt/G7j/NgEctUS6IweH1RGhURjjCc2NWtzXFPXXWWfc7hgbXQdtiQu2LGp6MxUg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/less/node_modules/performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha512-YHk5ez1hmMR5LOkb9iJkLKqoBlL7WD5M8ljC75ZfzXriuBIVNuecaXuU7e+hOwyqf24Wxhh7Vxgt7Hnw9288Tg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/less/node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/less/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/less/node_modules/qs": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.1.tgz", + "integrity": "sha512-LQy1Q1fcva/UsnP/6Iaa4lVeM49WiOitu2T4hZCyA/elLKu37L99qcBJk4VCCk+rdLvnMzfKyiN3SZTqdAZGSQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/less/node_modules/request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha512-IZnsR7voF0miGSu29EXPRgPTuEsI/+aibNSBbN1pplrfartF5wDYGADz3iD9vmBVf2r00rckWZf8BtS5kk7Niw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/less/node_modules/tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primereact": { + "version": "10.9.7", + "resolved": "https://registry.npmjs.org/primereact/-/primereact-10.9.7.tgz", + "integrity": "sha512-Ap/lg9GGaS8Pq7IIlzguuG3qlaU6PYF6E0cCRo0rnWauRw/SQGvfreSVIIxqEhtR6xqlf7OV759lyvVOvBzmsQ==", + "license": "MIT", + "dependencies": { + "@types/react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "peer": true, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha512-7bgVOAnPj3XjrKY577S+puCKGCRlUrcrEdsMeRXlg9Ghf5df/xNi6sONUa43WrHUd3TjJBF7O04jYoiY0FVa0A==", + "deprecated": "This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "dev": true, + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense", + "optional": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/vite": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz", + "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.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": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..41f66e5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "klangkiste-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@phosphor-icons/react": "2.1.7", + "primeicons": "^7.0.0", + "primereact": "^10.7.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "4.3.1", + "vite": "5.4.3" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..0df5da6 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,3515 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { + ArrowRight, + ArrowUp, + CaretDown, + CaretRight, + Cube, + ContactlessPayment, + FileAudio, + FloppyDisk, + Folder, + FolderOpen, + GearSix, + House, + PencilSimple, + Plus, + Trash, + UploadSimple, + Wrench, +} from '@phosphor-icons/react'; +import { Avatar } from 'primereact/avatar'; +import { Badge } from 'primereact/badge'; +import { Button } from 'primereact/button'; +import { Card } from 'primereact/card'; +import { Divider } from 'primereact/divider'; +import { Dropdown } from 'primereact/dropdown'; +import { FileUpload } from 'primereact/fileupload'; +import { InputText } from 'primereact/inputtext'; +import { Message } from 'primereact/message'; +import { MeterGroup } from 'primereact/metergroup'; +import { Messages } from 'primereact/messages'; +import { ProgressBar } from 'primereact/progressbar'; +import { Slider } from 'primereact/slider'; +import { Stepper } from 'primereact/stepper'; +import { StepperPanel } from 'primereact/stepperpanel'; +import { Tag } from 'primereact/tag'; +import { Toast } from 'primereact/toast'; +import { + assignTag, + claimTag, + createMediaFolder, + deleteMedia, + deleteTag, + getBoxes, + getBoxLocalTags, + getBoxTags, + getBoxDetails, + getBoxStorage, + getMediaTree, + getStatus, + getTagBlocks, + getTags, + markTagWritten, + moveMedia, + pairBox, + pullTagFromBox, + renameMedia, + sendCommand, + setBoxAlias, + setTagAlias, + setTagBlock, + setTagMedia, + unassignTag, + unpairBox, + uploadMedia, +} from './api.js'; + +const BOX_POLL_MS = 1500; +const STATUS_POLL_MS = 1000; +const STORAGE_POLL_MS = 5000; +const ONLINE_THRESHOLD_SEC = 60; + +const NAV_ITEMS = [ + { id: 'dashboard', label: 'Dashboard', icon: House }, + { id: 'boxes', label: 'Boxen', icon: Cube }, + { id: 'media', label: 'Medien', icon: FolderOpen }, + { id: 'tags', label: 'Tags', icon: ContactlessPayment }, + { id: 'settings', label: 'Einstellungen', icon: GearSix }, +]; + +function formatTime(ts) { + if (!ts) return '-'; + return new Date(ts * 1000).toLocaleString(); +} + +function getAvailability(lastSeen) { + if (!lastSeen) { + return { label: 'Offline', severity: 'danger' }; + } + const nowSec = Date.now() / 1000; + const isOnline = nowSec - lastSeen <= ONLINE_THRESHOLD_SEC; + return { label: isOnline ? 'Online' : 'Offline', severity: isOnline ? 'success' : 'danger' }; +} + +function parseCapabilities(raw) { + if (!raw) return '-'; + try { + const data = JSON.parse(raw); + return Object.entries(data) + .map(([key, value]) => `${key}:${value ? '1' : '0'}`) + .join(' '); + } catch (error) { + return '-'; + } +} + +function formatSize(bytes) { + if (bytes === 0) return '0 B'; + if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return '-'; + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let idx = 0; + while (size >= 1024 && idx < units.length - 1) { + size /= 1024; + idx += 1; + } + return `${size.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; +} + +function formatDuration(seconds) { + if (seconds === null || seconds === undefined || Number.isNaN(seconds)) return '-'; + const total = Math.max(0, Math.floor(seconds)); + const mins = Math.floor(total / 60); + const secs = total % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +function formatClock(seconds) { + if (seconds === null || seconds === undefined || Number.isNaN(seconds)) return '-'; + const total = Math.max(0, Math.floor(seconds)); + const hrs = Math.floor(total / 3600); + const mins = Math.floor((total % 3600) / 60); + const secs = total % 60; + if (hrs > 0) { + return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +function capacityScale(percent) { + const p = Math.max(0, Math.min(100, percent || 0)); + if (p === 0) return 1; + return 100 / p; +} + +function getNfcKey(nfc) { + if (!nfc) return ''; + const at = nfc.at || ''; + const uid = nfc.uid || ''; + const hardware = nfc.hardwareUid || nfc.hardware_uid || ''; + return `${at}:${uid}:${hardware}`; +} + +function generateTagId() { + const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let generated = ''; + for (let i = 0; i < 10; i += 1) { + generated += alphabet[Math.floor(Math.random() * alphabet.length)]; + } + return generated; +} + +function generateHardwareUid() { + const bytes = []; + for (let i = 0; i < 7; i += 1) { + bytes.push(Math.floor(Math.random() * 256)); + } + return bytes + .map((value) => value.toString(16).padStart(2, '0').toUpperCase()) + .join(':'); +} + +function isValidTagUid(uid) { + return /^(?:[a-z0-9]{10}|TAG_[a-z0-9]{8})$/.test(uid); +} + +function SectionHeader({ title, subtitle, actions }) { + return ( +
+
+

{title}

+

{subtitle}

+
+
{actions}
+
+ ); +} + +function StatGrid({ items }) { + return ( +
+ {items.map((card) => ( + +

{card.label}

+
{card.value}
+ {card.helper} +
+ ))} +
+ ); +} + +export default function App() { + const [activeSection, setActiveSection] = useState('dashboard'); + const [searchValue, setSearchValue] = useState(''); + const [boxes, setBoxes] = useState([]); + const [boxStorage, setBoxStorage] = useState({}); + const [selectedId, setSelectedId] = useState(''); + const [boxDetailsOpen, setBoxDetailsOpen] = useState(false); + const [boxDetailsLoading, setBoxDetailsLoading] = useState(false); + const [boxDetailsError, setBoxDetailsError] = useState(''); + const [boxDetailsData, setBoxDetailsData] = useState(null); + const [status, setStatus] = useState(null); + const [error, setError] = useState(''); + const [nfcUid, setNfcUid] = useState('UID_1'); + const [mediaTree, setMediaTree] = useState(null); + const [mediaError, setMediaError] = useState(''); + const [currentPath, setCurrentPath] = useState(''); + const [selectedPaths, setSelectedPaths] = useState([]); + const [newFolderName, setNewFolderName] = useState(''); + const [renameName, setRenameName] = useState(''); + const [moveTarget, setMoveTarget] = useState(''); + const [activeModal, setActiveModal] = useState(''); + const [tagDeleteTarget, setTagDeleteTarget] = useState(''); + const [expandedFolders, setExpandedFolders] = useState(() => new Set(['__root__'])); + const [sidebarQuery, setSidebarQuery] = useState(''); + const [uploadAfterCreate, setUploadAfterCreate] = useState(false); + const [pendingUploadFiles, setPendingUploadFiles] = useState([]); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadInProgress, setUploadInProgress] = useState(false); + const [activeUploadLabel, setActiveUploadLabel] = useState(''); + const [uploadSize, setUploadSize] = useState(0); + const [uploadNameError, setUploadNameError] = useState(''); + const transferTimerRef = useRef(null); + const explorerRef = useRef(null); + const modalRef = useRef(null); + const toastRef = useRef(null); + const fileUploadRef = useRef(null); + const dragSelectRef = useRef(false); + const lastDblClickRef = useRef(0); + const lastAnchorRef = useRef(''); + const explorerInitRef = useRef(false); + const expandedInitRef = useRef(false); + const volumeRef = useRef(null); + const [tags, setTags] = useState([]); + const [boxTags, setBoxTags] = useState([]); + const [blockedByBox, setBlockedByBox] = useState({}); + const [dbTagMedia, setDbTagMedia] = useState({}); + const [localBoxTags, setLocalBoxTags] = useState([]); + const [localBoxError, setLocalBoxError] = useState(''); + const [importTargetFolder, setImportTargetFolder] = useState(''); + const [importTargetUid, setImportTargetUid] = useState(''); + const [scanTagUid, setScanTagUid] = useState(''); + const [tagUidMode, setTagUidMode] = useState('keep'); + const [tagUidError, setTagUidError] = useState(''); + const [tagStep, setTagStep] = useState(0); + const [tagStepOneDone, setTagStepOneDone] = useState(false); + const [tagStepTwoDone, setTagStepTwoDone] = useState(false); + const [tagStepMax, setTagStepMax] = useState(0); + const tagStepperRef = useRef(null); + const tagWizardRestoredRef = useRef(''); + const tagUidMsgRef = useRef(null); + const mediaMsgRef = useRef(null); + const autoReplaceMsg = 'Ungueltige ID erkannt – wird automatisch ersetzt.'; + const [dismissedNfcKey, setDismissedNfcKey] = useState(''); + const [scanTagLabel, setScanTagLabel] = useState(''); + const [scanMediaPath, setScanMediaPath] = useState(''); + const [reuseTagUid, setReuseTagUid] = useState(''); + const lastNfcKeyRef = useRef(''); + const [tagAliasDrafts, setTagAliasDrafts] = useState({}); + const [boxAliasDrafts, setBoxAliasDrafts] = useState({}); + const [showSessionSheet, setShowSessionSheet] = useState(false); + const [drawerTab, setDrawerTab] = useState('boxes'); + const [boxDeleteTarget, setBoxDeleteTarget] = useState(''); + const [lastHardwareUid, setLastHardwareUid] = useState({ uid: '', hardwareUid: '' }); + const [simulatedNfc, setSimulatedNfc] = useState(null); + const [showVolume, setShowVolume] = useState(false); + const [volumeDraft, setVolumeDraft] = useState(50); + const [seekPercent, setSeekPercent] = useState(0); + const [isSeeking, setIsSeeking] = useState(false); + + function addToast(type, message) { + toastRef.current?.show({ + severity: type, + summary: type === 'error' ? 'Fehler' : 'Info', + detail: message, + life: 4000, + }); + } + + async function handleShowBoxDetails(boxId) { + setBoxDetailsOpen(true); + setBoxDetailsLoading(true); + setBoxDetailsError(''); + setBoxDetailsData(null); + const response = await getBoxDetails(boxId); + if (!response.ok) { + const message = response.data.detail || 'Box-Details konnten nicht geladen werden.'; + setBoxDetailsError(message); + addToast('error', message); + setBoxDetailsLoading(false); + return; + } + setBoxDetailsData(response.data || null); + setBoxDetailsLoading(false); + } + + useEffect(() => { + let active = true; + + async function refreshBoxes() { + const response = await getBoxes(); + if (!active) return; + if (!response.ok) { + setError(response.data.detail || 'Fehler beim Laden der Boxen.'); + return; + } + setBoxes(response.data.boxes || []); + setError(''); + } + + refreshBoxes(); + const handle = setInterval(refreshBoxes, BOX_POLL_MS); + return () => { + active = false; + clearInterval(handle); + }; + }, []); + + useEffect(() => { + let active = true; + + async function refreshStorage() { + if (!boxes.length) { + if (active) setBoxStorage({}); + return; + } + const results = await Promise.all( + boxes.map(async (box) => { + const response = await getBoxStorage(box.box_id); + if (!response.ok) { + return { boxId: box.box_id, storage: null }; + } + return { boxId: box.box_id, storage: response.data.storage || null }; + }) + ); + if (!active) return; + const next = {}; + results.forEach(({ boxId, storage }) => { + if (!storage) { + next[boxId] = null; + return; + } + const total = storage.total_bytes ?? null; + const free = storage.free_bytes ?? null; + const used = + storage.used_bytes ?? (total !== null && free !== null ? total - free : null); + next[boxId] = { total, free, used }; + }); + setBoxStorage(next); + } + + refreshStorage(); + const handle = setInterval(refreshStorage, STORAGE_POLL_MS); + return () => { + active = false; + clearInterval(handle); + }; + }, [boxes]); + + useEffect(() => { + function handleOutside(event) { + if (activeModal) return; + if (!explorerRef.current) return; + if (modalRef.current && modalRef.current.contains(event.target)) return; + if (!explorerRef.current.contains(event.target)) { + setSelectedPaths([]); + setRenameName(''); + } + } + function handleMouseUp() { + dragSelectRef.current = false; + } + window.addEventListener('mousedown', handleOutside); + window.addEventListener('mouseup', handleMouseUp); + return () => { + window.removeEventListener('mousedown', handleOutside); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [activeModal]); + + useEffect(() => { + if (!selectedId) { + setStatus(null); + setMediaTree(null); + setMediaError(''); + setSelectedPaths([]); + setNewFolderName(''); + setRenameName(''); + setActiveModal(''); + setBoxTags([]); + setDbTagMedia({}); + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + setReuseTagUid(''); + setLocalBoxTags([]); + setLocalBoxError(''); + return; + } + + let active = true; + + async function refreshStatus() { + const response = await getStatus(selectedId); + if (!active) return; + if (!response.ok) { + setStatus({ error: response.data.detail || 'Status nicht verfuegbar.' }); + return; + } + setStatus(response.data); + } + + refreshStatus(); + const handle = setInterval(refreshStatus, STATUS_POLL_MS); + return () => { + active = false; + clearInterval(handle); + }; + }, [selectedId]); + + useEffect(() => { + let active = true; + + async function refreshMedia() { + const response = await getMediaTree(); + if (!active) return; + if (!response.ok) { + setMediaError(response.data.detail || 'Medien nicht verfuegbar.'); + setMediaTree(null); + return; + } + setMediaTree(response.data); + setMediaError(''); + } + + refreshMedia(); + return () => { + active = false; + }; + }, []); + + function findPathChain(node, target, chain = []) { + if (!node) return null; + const currentPath = node.path || ''; + const nextChain = [...chain, currentPath]; + if (currentPath === target) { + return nextChain; + } + if (!Array.isArray(node.children)) return null; + for (const child of node.children) { + if (child.type !== 'folder') continue; + const result = findPathChain(child, target, nextChain); + if (result) return result; + } + return null; + } + + useEffect(() => { + if (!mediaTree) return; + const saved = localStorage.getItem('klangkiste_explorer_path'); + if (saved === null) return; + const chain = findPathChain(mediaTree, saved); + if (!chain) return; + setCurrentPath(saved); + if (!expandedInitRef.current) { + const savedExpandedRaw = localStorage.getItem('klangkiste_explorer_expanded'); + let savedExpanded = []; + if (savedExpandedRaw) { + try { + const parsed = JSON.parse(savedExpandedRaw); + if (Array.isArray(parsed)) { + savedExpanded = parsed; + } + } catch (error) { + savedExpanded = []; + } + } + const merged = new Set([ + ...savedExpanded, + ...chain.map((p) => (p ? p : '__root__')), + ]); + setExpandedFolders(merged); + expandedInitRef.current = true; + } + explorerInitRef.current = true; + }, [mediaTree]); + + useEffect(() => { + if (!explorerInitRef.current) return; + localStorage.setItem('klangkiste_explorer_path', currentPath || ''); + }, [currentPath]); + + useEffect(() => { + if (!expandedInitRef.current) return; + localStorage.setItem( + 'klangkiste_explorer_expanded', + JSON.stringify(Array.from(expandedFolders)) + ); + }, [expandedFolders]); + + useEffect(() => { + const lastNfc = status?.last_nfc; + if (!lastNfc || !lastNfc.uid) { + return; + } + if (!lastNfc.known) { + const key = `${status?.last_nfc_at ?? ''}:${lastNfc.uid ?? ''}`; + if (lastNfcKeyRef.current === key) { + return; + } + lastNfcKeyRef.current = key; + setScanTagLabel(''); + setScanMediaPath(''); + if (isValidTagUid(lastNfc.uid)) { + setScanTagUid(lastNfc.uid); + setTagUidMode('keep'); + setTagUidError(''); + } else { + setScanTagUid(generateTagId()); + setTagUidMode('new'); + setTagUidError(autoReplaceMsg); + } + setDismissedNfcKey(''); + setTagStep(0); + setTagStepOneDone(false); + setTagStepTwoDone(false); + setTagStepMax(0); + } + }, [status, tags]); + + useEffect(() => { + tagStepperRef.current?.setActiveStep(tagStep); + }, [tagStep]); + + useEffect(() => { + const ref = tagUidMsgRef.current; + if (!ref) return; + ref.clear(); + if (tagStep === 0 && tagUidError) { + const showMessage = () => { + if (!tagUidMsgRef.current) return false; + tagUidMsgRef.current.clear(); + tagUidMsgRef.current.show({ + severity: 'warn', + summary: 'Ungueltige ID', + detail: tagUidError, + sticky: true, + closable: false, + }); + return true; + }; + if (!showMessage()) { + const timer = setTimeout(() => { + showMessage(); + }, 0); + return () => clearTimeout(timer); + } + } + }, [tagUidError, tagStep]); + + useEffect(() => { + const ref = mediaMsgRef.current; + if (!ref) return; + ref.clear(); + if (tagStep === 1 && !scanMediaPath) { + const showMessage = () => { + if (!mediaMsgRef.current) return false; + mediaMsgRef.current.clear(); + mediaMsgRef.current.show({ + severity: 'info', + summary: 'Keine Medienzuordnung', + detail: 'Ohne Medienauswahl wird der Tag ohne Zuordnung gespeichert.', + sticky: true, + closable: false, + }); + return true; + }; + if (!showMessage()) { + const timer = setTimeout(() => { + showMessage(); + }, 0); + return () => clearTimeout(timer); + } + } + }, [scanMediaPath, tagStep]); + + useEffect(() => { + let active = true; + + async function refreshTags() { + const response = await getTags(); + if (!active) return; + if (!response.ok) { + setError(response.data.detail || 'Tags nicht verfuegbar.'); + return; + } + setTags(response.data.tags || []); + } + + refreshTags(); + return () => { + active = false; + }; + }, []); + + useEffect(() => { + if (!selectedId) return; + let active = true; + + async function refreshBoxTags() { + const response = await getBoxTags(selectedId); + if (!active) return; + if (!response.ok) { + setError(response.data.detail || 'Box-Tags nicht verfuegbar.'); + return; + } + setBoxTags(response.data.tags || []); + } + + refreshBoxTags(); + return () => { + active = false; + }; + }, [selectedId]); + + useEffect(() => { + if (!selectedId) return; + let active = true; + + async function refreshLocalTags() { + const response = await getBoxLocalTags(selectedId); + if (!active) return; + if (!response.ok) { + setLocalBoxError(response.data.detail || 'Lokale Box-Tags nicht verfuegbar.'); + return; + } + setLocalBoxTags(response.data.tags || []); + setLocalBoxError(''); + } + + refreshLocalTags(); + return () => { + active = false; + }; + }, [selectedId]); + + useEffect(() => { + let active = true; + + async function refreshBlocked() { + if (!boxes.length) { + setBlockedByBox({}); + return; + } + const results = await Promise.all( + boxes.map(async (box) => { + const response = await getTagBlocks(box.box_id); + return { boxId: box.box_id, response }; + }) + ); + if (!active) return; + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + + refreshBlocked(); + return () => { + active = false; + }; + }, [boxes]); + + const unpaired = useMemo( + () => boxes.filter((box) => box.state === 'UNPAIRED'), + [boxes] + ); + const paired = useMemo( + () => boxes.filter((box) => box.state === 'PAIRED'), + [boxes] + ); + + const mediaTagCounts = useMemo(() => { + const counts = {}; + tags.forEach((tag) => { + const mediaPath = tag.media_path || ''; + if (!mediaPath) return; + counts[mediaPath] = (counts[mediaPath] || 0) + 1; + }); + return counts; + }, [tags]); + + const topLevelFolders = useMemo(() => { + if (!mediaTree || !Array.isArray(mediaTree.children)) return []; + const folders = mediaTree.children.filter((child) => child.type === 'folder'); + const query = sidebarQuery.trim().toLowerCase(); + if (!query) return folders; + return folders.filter((folder) => (folder.name || '').toLowerCase().includes(query)); + }, [mediaTree, sidebarQuery]); + + const filteredTree = useMemo(() => { + if (!mediaTree) return null; + return { + ...mediaTree, + children: topLevelFolders, + }; + }, [mediaTree, topLevelFolders]); + + const dashboardStats = useMemo(() => { + const sizeLabel = mediaTree ? formatSize(mediaTree.size) : '-'; + const freeLabel = mediaTree ? formatSize(mediaTree.free_bytes) : '-'; + return [ + { + label: 'Gepairte Boxen', + value: paired.length, + helper: `${unpaired.length} neu`, + }, + { + label: 'Tags gesamt', + value: tags.length, + helper: `${boxTags.length} auf Box`, + }, + { + label: 'Medien gesamt', + value: sizeLabel, + helper: `frei ${freeLabel}`, + }, + { + label: 'Letzter NFC', + value: status?.last_nfc?.uid || '-', + helper: status?.last_nfc_at ? formatTime(status.last_nfc_at) : '-', + }, + ]; + }, [paired.length, unpaired.length, tags.length, boxTags.length, mediaTree, status]); + + async function handlePair(boxId) { + const response = await pairBox(boxId); + if (!response.ok) { + setError(response.data.detail || 'Box pairing fehlgeschlagen.'); + addToast('error', response.data.detail || 'Box pairing fehlgeschlagen.'); + return; + } + addToast('success', 'Box gepairt.'); + const updated = await getBoxes(); + if (updated.ok) { + setBoxes(updated.data.boxes || []); + } + } + + async function handleCommand(command, payload = {}) { + if (!selectedId) { + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + return; + } + let nextPayload = payload; + if (command === 'nfc_on' || command === 'nfc_off') { + const rawUid = typeof payload?.uid === 'string' ? payload.uid.trim() : ''; + let uid = rawUid; + if (!uid) { + if (command === 'nfc_on') { + setSimulatedNfc({ + uid: '', + known: false, + hardwareUid: generateHardwareUid(), + at: Date.now(), + }); + setScanTagLabel(''); + setScanMediaPath(''); + if (!scanTagUid) { + setScanTagUid(generateTagId()); + } + setTagUidMode('new'); + setTagStep(0); + setTagStepOneDone(false); + setTagStepTwoDone(false); + return; + } + setError('Bitte eine UID angeben.'); + addToast('error', 'Bitte eine UID angeben.'); + return; + } + nextPayload = { ...payload, uid }; + if (command === 'nfc_on') { + setLastHardwareUid({ uid, hardwareUid: generateHardwareUid() }); + setSimulatedNfc(null); + } + } + const response = await sendCommand(selectedId, command, nextPayload); + if (!response.ok) { + setError(response.data.detail || 'Command fehlgeschlagen.'); + addToast('error', response.data.detail || 'Command fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Command gesendet.'); + } + + async function handlePlayerCommand(command, payload = {}) { + if (!selectedId) { + return; + } + let nextPayload = payload; + if (command === 'nfc_on' || command === 'nfc_off') { + const rawUid = typeof payload?.uid === 'string' ? payload.uid.trim() : ''; + const uid = rawUid; + if (!uid) { + if (command === 'nfc_on') { + setSimulatedNfc({ + uid: '', + known: false, + hardwareUid: generateHardwareUid(), + at: Date.now(), + }); + setScanTagLabel(''); + setScanMediaPath(''); + if (!scanTagUid) { + setScanTagUid(generateTagId()); + } + setTagUidMode('new'); + setTagStep(0); + setTagStepOneDone(false); + setTagStepTwoDone(false); + setTagStepMax(0); + } else if (command === 'nfc_off') { + setSimulatedNfc(null); + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + setLastHardwareUid({ uid: '', hardwareUid: '' }); + setTagUidMode('keep'); + setTagStep(0); + setTagStepOneDone(false); + setTagStepTwoDone(false); + setTagStepMax(0); + } + return; + } + if (command === 'nfc_on') { + setLastHardwareUid({ uid, hardwareUid: generateHardwareUid() }); + setSimulatedNfc(null); + } + nextPayload = { ...payload, uid }; + } + await sendCommand(selectedId, command, nextPayload); + } + + async function handleUnpair(boxId) { + const response = await unpairBox(boxId); + if (!response.ok) { + setError(response.data.detail || 'Unpair fehlgeschlagen.'); + addToast('error', response.data.detail || 'Unpair fehlgeschlagen.'); + return; + } + addToast('success', 'Box unpaired.'); + const updated = await getBoxes(); + if (updated.ok) { + setBoxes(updated.data.boxes || []); + } + } + + async function handleMediaRefresh() { + const response = await getMediaTree(); + if (!response.ok) { + setMediaError(response.data.detail || 'Medien nicht verfuegbar.'); + setMediaTree(null); + return; + } + setMediaTree(response.data); + setMediaError(''); + } + + function getNodeByPath(node, targetPath) { + if (!node) return null; + if ((node.path || '') === targetPath) return node; + if (!Array.isArray(node.children)) return null; + for (const child of node.children) { + const found = getNodeByPath(child, targetPath); + if (found) return found; + } + return null; + } + + function listChildren(node) { + if (!node) return []; + if (!Array.isArray(node.children)) return []; + return node.children; + } + + function buildBreadcrumb(pathValue) { + if (!pathValue) return []; + const parts = pathValue.split('/').filter(Boolean); + return parts.map((part, index) => ({ + name: part, + path: parts.slice(0, index + 1).join('/'), + })); + } + + function collectTopLevelFolders(node) { + if (!node || !Array.isArray(node.children)) return []; + return node.children + .filter((child) => child.type === 'folder') + .map((child) => child.path); + } + + function collectFolderPaths(node) { + if (!node || node.type !== 'folder') return []; + const entries = [node.path || '']; + if (!Array.isArray(node.children)) return entries; + node.children.forEach((child) => { + if (child.type !== 'folder') return; + entries.push(...collectFolderPaths(child)); + }); + return entries; + } + + function isSelected(pathValue) { + return selectedPaths.includes(pathValue || ''); + } + + async function handleCreateFolder() { + const trimmedName = newFolderName.trim(); + if (uploadAfterCreate && pendingUploadFiles.length > 0) { + if (!trimmedName && !currentPath) { + addToast('error', 'Im Medien-Hauptordner ist ein Ordnername erforderlich.'); + setUploadNameError('Im Medien-Hauptordner ist ein Ordnername erforderlich.'); + return; + } + const targetPath = trimmedName ? `${currentPath}/${trimmedName}` : currentPath; + setUploadInProgress(true); + setActiveUploadLabel( + pendingUploadFiles.length ? `Upload: ${pendingUploadFiles.length} Datei(en)` : '' + ); + const uploadResponse = await uploadMedia( + targetPath, + pendingUploadFiles, + (percent) => setUploadProgress(percent) + ); + setUploadInProgress(false); + setUploadProgress(0); + if (!uploadResponse.ok) { + setMediaError(uploadResponse.data.detail || 'Upload fehlgeschlagen.'); + addToast('error', uploadResponse.data.detail || 'Upload fehlgeschlagen.'); + return; + } + addToast('success', 'Upload abgeschlossen.'); + await handleMediaRefresh(); + setActiveModal(''); + setUploadAfterCreate(false); + setPendingUploadFiles([]); + setNewFolderName(''); + return; + } + + if (!trimmedName && !uploadAfterCreate) { + setMediaError('Bitte einen Ordnernamen angeben.'); + return; + } + + const response = await createMediaFolder(currentPath, trimmedName); + if (!response.ok) { + setMediaError(response.data.detail || 'Ordner anlegen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Ordner anlegen fehlgeschlagen.'); + return; + } + addToast('success', 'Ordner angelegt.'); + setNewFolderName(''); + setActiveModal(''); + await handleMediaRefresh(); + } + + async function handleRename() { + if (!selectedPaths.length) return; + const response = await renameMedia(selectedPaths[0], renameName.trim()); + if (!response.ok) { + setMediaError(response.data.detail || 'Umbenennen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Umbenennen fehlgeschlagen.'); + return; + } + addToast('success', 'Eintrag umbenannt.'); + setRenameName(''); + setActiveModal(''); + await handleMediaRefresh(); + } + + async function handleDeleteSelected() { + if (!selectedPaths.length) return; + for (const pathValue of selectedPaths) { + const response = await deleteMedia(pathValue); + if (!response.ok) { + setMediaError(response.data.detail || 'Loeschen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Loeschen fehlgeschlagen.'); + return; + } + } + addToast('success', 'Eintraege geloescht.'); + setSelectedPaths([]); + setActiveModal(''); + await handleMediaRefresh(); + } + + async function handleMoveSelected() { + if (!selectedPaths.length) return; + const pathValue = selectedPaths[0]; + const isRootTarget = moveTarget === '__root__' || moveTarget === ''; + const response = await moveMedia(pathValue, isRootTarget ? '' : moveTarget); + if (!response.ok) { + setMediaError(response.data.detail || 'Verschieben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Verschieben fehlgeschlagen.'); + return; + } + addToast('success', 'Eintrag verschoben.'); + setMoveTarget(''); + setSelectedPaths([]); + setActiveModal(''); + await handleMediaRefresh(); + } + + async function handleUpload(event) { + const files = Array.from(event.target.files || []); + const audioFiles = files.filter((file) => file.type.startsWith('audio/')); + if (!audioFiles.length) return; + if (!currentPath) { + addToast('error', 'Im Medien-Hauptordner ist ein Ordnername erforderlich.'); + setUploadNameError('Im Medien-Hauptordner ist ein Ordnername erforderlich.'); + return; + } + setUploadInProgress(true); + setActiveUploadLabel( + audioFiles.length ? `Upload: ${audioFiles.length} Datei(en)` : '' + ); + const response = await uploadMedia(currentPath, audioFiles, (percent) => + setUploadProgress(percent) + ); + setUploadInProgress(false); + setUploadProgress(0); + if (!response.ok) { + setMediaError(response.data.detail || 'Upload fehlgeschlagen.'); + addToast('error', response.data.detail || 'Upload fehlgeschlagen.'); + return; + } + addToast('success', 'Upload abgeschlossen.'); + await handleMediaRefresh(); + } + + const fileHeaderTemplate = (options) => { + const { className, chooseButton, cancelButton } = options; + return ( +
+ {chooseButton} + {cancelButton} +
+ ); + }; + + const fileItemTemplate = (file, props) => ( +
+
+ ); + + const fileEmptyTemplate = () => ( +
+ Gesamt: {formatSize(uploadSize)} +
+ ); + + function handleSelect(item, event) { + const itemPath = item.path || ''; + if (event.shiftKey && lastAnchorRef.current) { + const items = listChildren(getNodeByPath(mediaTree, currentPath)); + const anchorIndex = items.findIndex((entry) => entry.path === lastAnchorRef.current); + const currentIndex = items.findIndex((entry) => entry.path === itemPath); + if (anchorIndex !== -1 && currentIndex !== -1) { + const [start, end] = anchorIndex < currentIndex + ? [anchorIndex, currentIndex] + : [currentIndex, anchorIndex]; + const range = items.slice(start, end + 1).map((entry) => entry.path || ''); + setSelectedPaths(Array.from(new Set([...selectedPaths, ...range]))); + return; + } + } + if (event.metaKey || event.ctrlKey) { + setSelectedPaths((prev) => + prev.includes(itemPath) + ? prev.filter((pathValue) => pathValue !== itemPath) + : [...prev, itemPath] + ); + lastAnchorRef.current = itemPath; + return; + } + const now = Date.now(); + const isDoubleClick = now - lastDblClickRef.current < 250; + if (!isDoubleClick) { + setSelectedPaths([itemPath]); + lastAnchorRef.current = itemPath; + } + } + + function handleDragSelect(item) { + if (!dragSelectRef.current) return; + const itemPath = item.path || ''; + setSelectedPaths((prev) => + prev.includes(itemPath) ? prev : [...prev, itemPath] + ); + } + + function handleOpen(item) { + if (item.type !== 'folder') return; + setCurrentPath(item.path || ''); + setSelectedPaths([]); + setRenameName(''); + const key = item.path || '__root__'; + setExpandedFolders((prev) => new Set(prev).add(key)); + } + + function handleGoUp() { + if (!currentPath) return; + const parts = currentPath.split('/').filter(Boolean); + parts.pop(); + const next = parts.join('/'); + setCurrentPath(next); + setSelectedPaths([]); + setRenameName(''); + } + + function toggleFolder(pathValue) { + const key = pathValue || '__root__'; + setExpandedFolders((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + } + + function renderFolderTree(node, depth = 0) { + if (!node || node.type !== 'folder') return null; + const isActive = (node.path || '') === currentPath; + const key = node.path || '__root__'; + const isExpanded = expandedFolders.has(key); + const childFolders = + Array.isArray(node.children) && node.children.filter((child) => child.type === 'folder'); + const hasChildren = childFolders && childFolders.length > 0; + const tagCount = mediaTagCounts[node.path || ''] || 0; + return ( +
+
handleOpen(node)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') handleOpen(node); + }} + > + {hasChildren ? ( +
{ + event.stopPropagation(); + toggleFolder(node.path || ''); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.stopPropagation(); + toggleFolder(node.path || ''); + } + }} + aria-label="Toggle" + > + {isExpanded ? : } +
+ ) : ( +
+ {isExpanded && + hasChildren && + childFolders.map((child) => renderFolderTree(child, depth + 1))} +
+ ); + } + + async function handleClaimTagForScan() { + setTagUidError(''); + const currentUid = scanTagUid.trim(); + const existing = tags.find((tag) => tag.uid === currentUid); + if (existing) { + if (existing.status === 'NEW') { + const written = await markTagWritten(currentUid); + if (!written.ok) { + if (written.data.detail === 'invalid uid') { + setTagUidError( + 'UID ungueltig. Bitte 10 Zeichen (a-z, 0-9) oder TAG_ + 8 Zeichen verwenden.' + ); + return; + } + setError(written.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', written.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + } + } else { + const response = await claimTag(currentUid, scanTagLabel.trim()); + if (!response.ok) { + if (response.data.detail === 'invalid uid') { + setTagUidError( + 'UID ungueltig. Bitte 10 Zeichen (a-z, 0-9) oder TAG_ + 8 Zeichen verwenden.' + ); + return; + } + setError(response.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + setScanTagUid(response.data.uid || ''); + } + setError(''); + addToast('success', 'Tag geschrieben.'); + if (activeNfc) { + setDismissedNfcKey(getNfcKey(activeNfc)); + } + const shouldAssign = + selectedId && status?.last_nfc?.known === false && scanMediaPath; + if (shouldAssign) { + const mediaSet = await setTagMedia(currentUid, scanMediaPath); + if (!mediaSet.ok) { + setError(mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + addToast('error', mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + return; + } + const assigned = await assignTag(currentUid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + addToast('success', 'Tag zugeordnet.'); + if (activeNfc?.known === false) { + await handlePlayerCommand('nfc_on', { uid: currentUid }); + } + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + setSimulatedNfc(null); + setLastHardwareUid({ uid: '', hardwareUid: '' }); + } else if (selectedId && status?.last_nfc?.known === false) { + addToast('success', 'Tag gespeichert. Medium fehlt noch.'); + setSimulatedNfc(null); + setLastHardwareUid({ uid: '', hardwareUid: '' }); + } + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + } + + async function handleReuseImportedTag() { + if (!reuseTagUid) { + setError('Bitte eine gespeicherte Tag-ID waehlen.'); + addToast('error', 'Bitte eine gespeicherte Tag-ID waehlen.'); + return; + } + if (!selectedId) { + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + return; + } + const tag = tags.find((entry) => entry.uid === reuseTagUid); + if (!tag || !tag.media_path) { + setError('Tag hat keine Medienzuordnung.'); + addToast('error', 'Tag hat keine Medienzuordnung.'); + return; + } + const response = await markTagWritten(reuseTagUid); + if (!response.ok) { + setError(response.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + const assigned = await assignTag(reuseTagUid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + addToast('success', 'Tag geschrieben und zugeordnet.'); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + setScanTagUid(reuseTagUid); + setReuseTagUid(''); + } + + async function handleWriteTag(uid) { + const response = await markTagWritten(uid); + if (!response.ok) { + setError(response.data.detail || 'Tag schreiben fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag schreiben fehlgeschlagen.'); + return; + } + addToast('success', 'Tag geschrieben.'); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + } + + async function handleStoreTagOnly() { + if (!activeNfc?.uid) { + setError('Keine UID erkannt.'); + addToast('error', 'Keine UID erkannt.'); + return; + } + const existing = tags.find((tag) => tag.uid === activeNfc.uid); + if (existing) { + addToast('success', 'Tag ist bereits in der Datenbank.'); + if (activeNfc) { + setDismissedNfcKey(getNfcKey(activeNfc)); + } + return; + } + const response = await claimTag(activeNfc.uid, ''); + if (!response.ok) { + setError(response.data.detail || 'Tag speichern fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag speichern fehlgeschlagen.'); + return; + } + addToast('success', 'Tag in der Datenbank gespeichert.'); + if (activeNfc) { + setDismissedNfcKey(getNfcKey(activeNfc)); + } + setSimulatedNfc(null); + setLastHardwareUid({ uid: '', hardwareUid: '' }); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + } + + async function handleAssignFromScan() { + if (!scanMediaPath) { + setError('Bitte zuerst einen Medienordner waehlen.'); + addToast('error', 'Bitte zuerst einen Medienordner waehlen.'); + return; + } + const uid = scanTagUid.trim(); + if (!uid) { + setError('Bitte zuerst eine Tag-ID schreiben.'); + addToast('error', 'Bitte zuerst eine Tag-ID schreiben.'); + return; + } + const matching = tags.find((tag) => tag.uid === uid); + if (!matching) { + setError('Tag-ID existiert nicht. Bitte zuerst schreiben.'); + addToast('error', 'Tag-ID existiert nicht. Bitte zuerst schreiben.'); + return; + } + const mediaSet = await setTagMedia(uid, scanMediaPath); + if (!mediaSet.ok) { + setError(mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + addToast('error', mediaSet.data.detail || 'Medium setzen fehlgeschlagen.'); + return; + } + const assigned = await assignTag(uid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + addToast('success', 'Tag zugeordnet.'); + if (activeNfc?.known === false) { + await handlePlayerCommand('nfc_on', { uid }); + } + if (activeNfc) { + setDismissedNfcKey(getNfcKey(activeNfc)); + } + setScanTagUid(''); + setScanTagLabel(''); + setScanMediaPath(''); + setSimulatedNfc(null); + setLastHardwareUid({ uid: '', hardwareUid: '' }); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + + async function handlePullTagFromBox() { + if (!selectedId) { + setError('Bitte zuerst eine Box auswaehlen.'); + addToast('error', 'Bitte zuerst eine Box auswaehlen.'); + return; + } + if (!importTargetUid) { + setError('Kein Tag ausgewaehlt.'); + addToast('error', 'Kein Tag ausgewaehlt.'); + return; + } + const response = await pullTagFromBox( + selectedId, + importTargetUid, + importTargetFolder.trim() + ); + if (!response.ok) { + setError(response.data.detail || 'Import fehlgeschlagen.'); + addToast('error', response.data.detail || 'Import fehlgeschlagen.'); + return; + } + addToast('success', 'Medien vom Box-Tag uebertragen.'); + setImportTargetUid(''); + setImportTargetFolder(''); + setActiveModal(''); + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + const updatedLocal = await getBoxLocalTags(selectedId); + if (updatedLocal.ok) { + setLocalBoxTags(updatedLocal.data.tags || []); + } + await handleMediaRefresh(); + } + + async function handleUnassignTag(uid) { + const response = await unassignTag(uid, selectedId); + if (!response.ok) { + setError(response.data.detail || 'Tag loesen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag loesen fehlgeschlagen.'); + return; + } + addToast('success', 'Tag getrennt.'); + const updated = await getBoxTags(selectedId); + if (updated.ok) { + setBoxTags(updated.data.tags || []); + } + } + + async function handleDeleteTag(uid) { + const response = await deleteTag(uid); + if (!response.ok) { + setError(response.data.detail || 'Tag loeschen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Tag loeschen fehlgeschlagen.'); + return; + } + addToast('success', 'Tag geloescht.'); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + if (boxes.length) { + const results = await Promise.all( + boxes.map(async (box) => ({ + boxId: box.box_id, + response: await getTagBlocks(box.box_id), + })) + ); + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + } + + async function handleSetTagMedia(uid) { + const mediaPath = dbTagMedia[uid] || ''; + const response = await setTagMedia(uid, mediaPath); + if (!response.ok) { + setError(response.data.detail || 'Medium setzen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Medium setzen fehlgeschlagen.'); + return; + } + const lastNfcUid = status?.last_nfc?.uid || ''; + const shouldAssign = + selectedId && + status?.last_nfc?.known === false && + (lastNfcUid === uid || (!lastNfcUid && scanTagUid && scanTagUid === uid)); + if (shouldAssign) { + const assigned = await assignTag(uid, selectedId); + if (!assigned.ok) { + setError(assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + addToast('error', assigned.data.detail || 'Zuordnung fehlgeschlagen.'); + return; + } + addToast('success', 'Medium gespeichert und Tag zugeordnet.'); + } else { + addToast('success', 'Medium gespeichert.'); + } + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + if (boxes.length) { + const results = await Promise.all( + boxes.map(async (box) => ({ + boxId: box.box_id, + response: await getTagBlocks(box.box_id), + })) + ); + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + } + + async function handleClearTagMedia(uid) { + const response = await setTagMedia(uid, ''); + if (!response.ok) { + setError(response.data.detail || 'Medium entfernen fehlgeschlagen.'); + addToast('error', response.data.detail || 'Medium entfernen fehlgeschlagen.'); + return; + } + setError(''); + addToast('success', 'Medienzuweisung entfernt.'); + setDbTagMedia((prev) => ({ ...prev, [uid]: '' })); + const updatedTags = await getTags(); + if (updatedTags.ok) { + setTags(updatedTags.data.tags || []); + } + if (selectedId) { + const updatedBoxTags = await getBoxTags(selectedId); + if (updatedBoxTags.ok) { + setBoxTags(updatedBoxTags.data.tags || []); + } + } + if (boxes.length) { + const results = await Promise.all( + boxes.map(async (box) => ({ + boxId: box.box_id, + response: await getTagBlocks(box.box_id), + })) + ); + const next = {}; + results.forEach(({ boxId, response }) => { + if (response.ok) { + next[boxId] = response.data.blocked || []; + } + }); + setBlockedByBox(next); + } + } + + async function handleToggleTagBlock(boxId, uid, nextBlocked) { + const response = await setTagBlock(boxId, uid, nextBlocked); + if (!response.ok) { + addToast('error', response.data.detail || 'Tag-Sperre fehlgeschlagen.'); + return; + } + setBlockedByBox((prev) => { + const current = new Set(prev[boxId] || []); + if (nextBlocked) { + current.add(uid); + } else { + current.delete(uid); + } + return { ...prev, [boxId]: Array.from(current) }; + }); + addToast( + 'success', + nextBlocked ? 'Tag gesperrt.' : 'Tag freigegeben.' + ); + } + + async function handleSaveTagAlias(uid) { + const value = (tagAliasDrafts[uid] ?? '').trim(); + const response = await setTagAlias(uid, value || null); + if (!response.ok) { + addToast('error', response.data.detail || 'Tag-Alias speichern fehlgeschlagen.'); + return; + } + const updated = await getTags(); + if (updated.ok) { + setTags(updated.data.tags || []); + } + addToast('success', 'Tag-Alias gespeichert.'); + } + + async function handleSaveBoxAlias(boxId) { + const value = (boxAliasDrafts[boxId] ?? '').trim(); + const response = await setBoxAlias(boxId, value || null); + if (!response.ok) { + addToast('error', response.data.detail || 'Box-Alias speichern fehlgeschlagen.'); + return; + } + const updated = await getBoxes(); + if (updated.ok) { + setBoxes(updated.data.boxes || []); + } + addToast('success', 'Box-Alias gespeichert.'); + } + + const currentItems = mediaTree + ? listChildren(getNodeByPath(mediaTree, currentPath)).filter( + (item) => !(item.name || '').startsWith('.') + ) + : []; + const showMeta = currentItems.some((item) => item.type === 'file'); + const mediaBytes = mediaTree?.size ?? null; + const freeBytes = mediaTree?.free_bytes ?? null; + const systemStorage = useMemo(() => { + if (!mediaTree || mediaTree.size === undefined || mediaTree.free_bytes === undefined) { + return null; + } + const used = Math.max(0, mediaTree.size || 0); + const free = Math.max(0, mediaTree.free_bytes || 0); + const total = used + free; + const percent = total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0; + return { used, free, total, percent, scale: capacityScale(percent) }; + }, [mediaTree]); + + const activeMeta = useMemo( + () => NAV_ITEMS.find((item) => item.id === activeSection), + [activeSection] + ); + const activeBoxLabel = useMemo(() => { + const activeBox = boxes.find((box) => box.box_id === selectedId); + return activeBox?.alias || activeBox?.box_id || ''; + }, [boxes, selectedId]); + const activeNfc = useMemo(() => { + if (status?.last_nfc && status.last_nfc.known === false) { + return { ...status.last_nfc, at: status.last_nfc_at }; + } + return simulatedNfc; + }, [status, simulatedNfc]); + + const activeNfcKey = useMemo( + () => (activeNfc ? getNfcKey(activeNfc) : ''), + [activeNfc] + ); + + useEffect(() => { + if (!activeNfcKey) return; + if (tagWizardRestoredRef.current === activeNfcKey) return; + try { + const raw = sessionStorage.getItem('klangkiste_tag_wizard'); + if (!raw) return; + const parsed = JSON.parse(raw); + if (!parsed || parsed.key !== activeNfcKey) return; + const data = parsed.data || {}; + if (typeof data.scanTagUid === 'string') setScanTagUid(data.scanTagUid); + if (typeof data.scanTagLabel === 'string') setScanTagLabel(data.scanTagLabel); + if (typeof data.scanMediaPath === 'string') setScanMediaPath(data.scanMediaPath); + if (typeof data.tagUidMode === 'string') setTagUidMode(data.tagUidMode); + if (typeof data.tagStep === 'number') setTagStep(data.tagStep); + setTagStepOneDone(Boolean(data.tagStepOneDone)); + setTagStepTwoDone(Boolean(data.tagStepTwoDone)); + if (typeof data.tagStepMax === 'number') setTagStepMax(data.tagStepMax); + if (typeof data.tagUidError === 'string') setTagUidError(data.tagUidError); + if (data.lastHardwareUid?.uid || data.lastHardwareUid?.hardwareUid) { + setLastHardwareUid(data.lastHardwareUid); + } + if (!status?.last_nfc && data.simulatedNfc) { + setSimulatedNfc(data.simulatedNfc); + } + if (typeof data.dismissedNfcKey === 'string') { + setDismissedNfcKey(data.dismissedNfcKey); + } + tagWizardRestoredRef.current = activeNfcKey; + } catch (error) { + // ignore storage restore errors + } + }, [activeNfcKey, status?.last_nfc]); + + useEffect(() => { + if (!activeNfcKey) return; + try { + const payload = { + key: activeNfcKey, + data: { + scanTagUid, + scanTagLabel, + scanMediaPath, + tagUidMode, + tagStep, + tagStepOneDone, + tagStepTwoDone, + tagStepMax, + tagUidError, + lastHardwareUid, + simulatedNfc, + dismissedNfcKey, + }, + }; + sessionStorage.setItem('klangkiste_tag_wizard', JSON.stringify(payload)); + } catch (error) { + // ignore storage write errors + } + }, [ + activeNfcKey, + scanTagUid, + scanTagLabel, + scanMediaPath, + tagUidMode, + tagStep, + tagStepOneDone, + tagStepTwoDone, + tagStepMax, + tagUidError, + lastHardwareUid, + simulatedNfc, + dismissedNfcKey, + ]); + const statusWithHardware = useMemo(() => { + if (!status) return null; + const hardwareUid = + lastHardwareUid.uid === status?.last_nfc?.uid && lastHardwareUid.hardwareUid + ? lastHardwareUid.hardwareUid + : activeNfc?.hardwareUid || null; + if (!status.last_nfc) return status; + return { + ...status, + last_nfc: { + ...status.last_nfc, + hardware_uid: hardwareUid, + }, + }; + }, [status, lastHardwareUid, activeNfc]); + const activeTag = useMemo(() => { + const uid = status?.last_nfc?.uid || activeNfc?.uid || ''; + if (!uid) return null; + return tags.find((tag) => tag.uid === uid) || null; + }, [status, activeNfc, tags]); + const playlistRoot = activeTag?.media_path || ''; + const playlistTitle = playlistRoot ? playlistRoot.split('/').filter(Boolean).pop() : ''; + const playlistItems = useMemo(() => { + if (!mediaTree || !playlistRoot) return []; + const node = getNodeByPath(mediaTree, playlistRoot); + if (!node) return []; + const stack = [node]; + const collected = []; + while (stack.length) { + const current = stack.pop(); + if (!current) continue; + if (current.type === 'file') { + collected.push(current); + continue; + } + if (Array.isArray(current.children)) { + for (let i = current.children.length - 1; i >= 0; i -= 1) { + stack.push(current.children[i]); + } + } + } + return collected; + }, [mediaTree, playlistRoot]); + const playlistDuration = useMemo(() => { + if (!playlistItems.length) return 0; + return playlistItems.reduce((sum, item) => sum + (item.duration || 0), 0); + }, [playlistItems]); + const playbackState = status?.playback_state || {}; + const currentIndex = + typeof playbackState.file_index === 'number' ? playbackState.file_index : null; + const currentTrack = + currentIndex !== null && playlistItems[currentIndex] + ? playlistItems[currentIndex] + : null; + const currentTitle = + currentTrack?.title || + currentTrack?.name || + (playbackState.current_file + ? playbackState.current_file.split('/').pop() + : ''); + const currentArtist = currentTrack?.artist || ''; + const playbackPosition = + typeof playbackState.position === 'number' ? playbackState.position : 0; + const playbackDuration = + typeof playbackState.duration === 'number' ? playbackState.duration : 0; + const playbackProgress = + playbackDuration > 0 + ? Math.min(100, Math.round((playbackPosition / playbackDuration) * 100)) + : 0; + const isPlaying = playbackState.state === 'PLAYING'; + const volumeValue = + typeof playbackState.volume === 'number' ? playbackState.volume : null; + + function handleSeekCommit(percent) { + if (!playbackDuration) return; + const targetSeconds = Math.round((percent / 100) * playbackDuration); + handlePlayerCommand('seek', { position: targetSeconds }); + } + + function handleVolumeCommit(nextValue) { + handlePlayerCommand('set_volume', { volume: nextValue }); + } + + useEffect(() => { + if (isSeeking) return; + setSeekPercent(playbackProgress); + }, [playbackProgress, isSeeking]); + + useEffect(() => { + setIsSeeking(false); + setSeekPercent(playbackProgress); + }, [currentIndex, playbackProgress]); + + useEffect(() => { + setVolumeDraft(volumeValue ?? 50); + }, [volumeValue]); + + useEffect(() => { + if (!showVolume) return; + function handleOutside(event) { + if (!volumeRef.current) return; + if (volumeRef.current.contains(event.target)) return; + setShowVolume(false); + } + document.addEventListener('mousedown', handleOutside); + document.addEventListener('touchstart', handleOutside); + return () => { + document.removeEventListener('mousedown', handleOutside); + document.removeEventListener('touchstart', handleOutside); + }; + }, [showVolume]); + + function renderSection() { + if (activeSection === 'dashboard') { + return ( +
+ +
+ +

Live-Status

+ {!selectedId &&

Waehle eine gepairte Box aus.

} + {selectedId && !status &&

Status wird geladen...

} + {status && status.error &&

{status.error}

} + {status && !status.error && ( +
{JSON.stringify(statusWithHardware, null, 2)}
+ )} +
+ +
+
+
+
+

{currentTitle || playlistTitle || 'Playlist'}

+

{currentArtist || playlistRoot || 'Kein Medium zugeordnet'}

+
+ {playlistItems.length} Tracks + {formatClock(playlistDuration)} +
+
+
+
+
+
+
+ {formatClock(playbackPosition)} + { + setSeekPercent(event.value ?? 0); + setIsSeeking(true); + }} + onSlideEnd={(event) => { + setIsSeeking(false); + handleSeekCommit(event.value ?? 0); + }} + /> + {formatClock(playbackDuration)} +
+
+ setNfcUid(event.target.value)} + placeholder="UID_1" + className="p-inputtext-sm" + /> +
+
+
+ # + Titel + Interpret + Dauer +
+ {playlistItems.length === 0 && ( +
+ + Keine Playlist gefunden. + + +
+ )} + {playlistItems.map((track, index) => ( +
{ + event.preventDefault(); + handlePlayerCommand('jump_to_index', { index }); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') { + handlePlayerCommand('jump_to_index', { index }); + } + }} + > + {index + 1} + {track.title || track.name} + {track.artist || '-'} + {formatClock(track.duration)} +
+ ))} +
+

+ Steuerung ist nur moeglich, wenn die Box gepairt ist. +

+
+
+
+
+ ); + } + + if (activeSection === 'boxes') { + return ( +
+ {unpaired.length > 0 && ( + +

Neue Boxen

+ {unpaired.map((box) => ( +
+
+ {box.alias || box.box_id} +
Zuletzt gesehen: {formatTime(box.last_seen)}
+
Firmware: {box.firmware_version}
+
ID: {box.box_id}
+
+
+ ))} +
+ )} + +

Gepairte Boxen

+ {paired.length === 0 &&

Noch keine gepairten Boxen.

} + {paired.length > 0 && ( +
+ {paired.map((box) => ( +
setSelectedId(box.box_id)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') setSelectedId(box.box_id); + }} + > +
+
+ {box.alias || box.box_id} + {getAvailability(box.last_seen).label} +
+
Zuletzt gesehen: {formatTime(box.last_seen)}
+
Capabilities: {parseCapabilities(box.capabilities_json)}
+
ID: {box.box_id}
+
+
+
+ event.stopPropagation()} + onChange={(event) => { + event.stopPropagation(); + setBoxAliasDrafts((prev) => ({ + ...prev, + [box.box_id]: event.target.value, + })); + }} + /> +
+
+
+ ))} +
+ )} +
+
+ ); + } + + if (activeSection === 'media') { + return ( +
+ +
+

Medien Explorer

+
+ {uploadInProgress && ( +
+ {activeUploadLabel || 'Upload laeuft...'} +
+
+
+
+ )} +

Ordnerverwaltung, Upload und Datei-Listen im Server-Medienordner.

+ {mediaError &&

{mediaError}

} + {!mediaError && !mediaTree &&

Medienliste wird geladen...

} + {!mediaError && mediaTree && ( +
+
+
+ setSidebarQuery(event.target.value)} + aria-label="Ordner suchen" + /> +
+
+ {topLevelFolders.length === 0 && ( +
Keine Ordner gefunden.
+ )} + {filteredTree && renderFolderTree(filteredTree, 0)} +
+
+
+
+
+
+
+
+ Name + {showMeta ? ( + <> + Interpret + Titel + Laenge + Groesse + + ) : ( + <> + Typ + Groesse + + )} +
+ {currentItems.map((item) => ( +
{ + if (event.button !== 0) return; + if (event.metaKey || event.ctrlKey || event.shiftKey) { + return; + } + dragSelectRef.current = true; + handleSelect(item, event); + }} + onMouseEnter={() => handleDragSelect(item)} + onClick={(event) => handleSelect(item, event)} + onDoubleClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + lastDblClickRef.current = Date.now(); + handleOpen(item); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter') handleOpen(item); + }} + > + + + {item.type === 'folder' ? ( + + ) : ( + + )} + + {item.name} + + {showMeta ? ( + <> + {item.type === 'folder' ? '-' : item.artist || '-'} + {item.type === 'folder' ? '-' : item.title || '-'} + + {item.type === 'folder' ? '-' : formatDuration(item.duration)} + + {formatSize(item.size)} + + ) : ( + <> + {item.type === 'folder' ? 'Ordner' : 'Datei'} + {formatSize(item.size)} + + )} +
+ ))} + {selectedPaths.length > 0 && ( +
+ + {showMeta ? ( + <> + + + + + {selectedPaths.length} ausgewaehlt + + + ) : ( + <> + + + {selectedPaths.length} ausgewaehlt + + + )} +
+ )} +
+
+
+ + Medienordner: {formatSize(mediaBytes)} · Verfuegbar:{' '} + {formatSize(freeBytes)} + +
+
+ )} +
+
+ ); + } + + if (activeSection === 'tags') { + return ( +
+ {(localBoxError || localBoxTags.length > 0) && ( + +
+

Tags nur auf dieser Box

+
+ {localBoxError &&

{localBoxError}

} + {localBoxTags.map((tag) => ( +
+
+ {tag.uid} +
+ Dateien: {tag.file_count} · {formatSize(tag.total_size)} +
+ {tag.media_exists && tag.files?.length > 0 && ( +
    + {tag.files.map((file) => ( +
  • {file}
  • + ))} +
+ )} + {!tag.media_exists && ( +

Keine Dateien im Box-Ordner gefunden.

+ )} +
+
+
+
+ ))} +
+ )} + + +

Tags (Datenbank)

+ {tags.length === 0 &&

Keine Tags vorhanden.

} + {tags.length > 0 && ( +
+ {tags.map((tag) => ( +
+
+
+ + {tag.alias ? `${tag.alias} (${tag.uid})` : tag.uid} + +
Status: {tag.status}
+
Medium: {tag.media_path || '-'}
+ {tag.label ?
Label: {tag.label}
: null} +
+
+
+ + setTagAliasDrafts((prev) => ({ + ...prev, + [tag.uid]: event.target.value, + })) + } + /> +
+
+ + setDbTagMedia((prev) => ({ + ...prev, + [tag.uid]: event.value, + })) + } + options={collectTopLevelFolders(mediaTree).map((folderPath) => ({ + label: folderPath, + value: folderPath, + }))} + placeholder="Medienordner waehlen" + filter + className="p-inputtext-sm" + /> +
+
+
+
+ ))} +
+ )} +
+ + +

Tag-Matrix (Sperren)

+ {paired.length === 0 && ( +

Keine gepairten Boxen vorhanden.

+ )} + {tags.length === 0 &&

Keine Tags vorhanden.

} + {paired.length > 0 && tags.length > 0 && ( +
+
+ Tag + {paired.map((box) => ( + + {box.alias || box.box_id} + + ))} +
+ {tags.map((tag) => ( +
+ {tag.alias || tag.uid} + {paired.map((box) => { + const blocked = (blockedByBox[box.box_id] || []).includes(tag.uid); + return ( + + handleToggleTagBlock(box.box_id, tag.uid, !blocked) + } + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleToggleTagBlock(box.box_id, tag.uid, !blocked); + } + }} + /> + ); + })} +
+ ))} +
+ )} +
+
+ ); + } + + return ( +
+ +

System

+

Backend: {import.meta.env.VITE_BACKEND_URL || 'http://127.0.0.1:5001'}

+
+ Gepairte Boxen + {paired.length} +
+
+ Tags + {tags.length} +
+
+
+ ); + } + + const systemPanels = ( + <> + +
+

Live Status

+ +
+

System Health und wichtige Events.

+
+ API + +
+
+ Speicher + +
+
+ Letzter Sync + {status?.last_sync_at ? formatTime(status.last_sync_at) : '-'} +
+
+ Hardware-UID + + {lastHardwareUid.uid === status?.last_nfc?.uid && lastHardwareUid.hardwareUid + ? lastHardwareUid.hardwareUid + : activeNfc?.hardwareUid || '-'} + +
+
+ +

Speicherkapazitaet

+
+
+ System + + {systemStorage + ? `${formatSize(systemStorage.used)} / ${formatSize(systemStorage.total)}` + : '-'} + +
+ +
+ Frei: {systemStorage ? formatSize(systemStorage.free) : '-'} +
+
+ + {boxes.length === 0 &&

Keine Boxen verbunden.

} + {boxes.map((box) => { + const storage = boxStorage[box.box_id] || null; + const used = storage?.used ?? null; + const free = storage?.free ?? null; + const total = storage?.total ?? null; + const totalValue = + total !== null ? total : used !== null && free !== null ? used + free : null; + const usedValue = + used !== null ? used : totalValue !== null && free !== null ? totalValue - free : null; + const percent = + totalValue && usedValue !== null + ? Math.min(100, Math.round((usedValue / totalValue) * 100)) + : 0; + const scale = capacityScale(percent); + return ( +
+
+ {box.alias || box.box_id} + + {totalValue !== null && usedValue !== null + ? `${formatSize(usedValue)} / ${formatSize(totalValue)}` + : '-'} + +
+ +
+ {free !== null ? `Frei: ${formatSize(free)}` : 'Keine Kapazitaet gemeldet'} +
+
+ ); + })} +
+ + ); + + return ( +
+
+
+
K
+
+ Klangkiste + PrimeReact GUI +
+
+ {selectedId && ( +
+ + {activeBoxLabel || selectedId} +
+ )} +
+
+
+ + + +
+ + +
+ + {error &&
{error}
} + {activeNfc && dismissedNfcKey !== getNfcKey(activeNfc) && ( +
+ +

+ {activeNfc.uid && + tags.find((tag) => tag.uid === activeNfc.uid && !tag.media_path) + ? 'Leerer Tag erkannt' + : 'Neuer Tag erkannt'} +

+

+ {activeNfc.uid ? ( + <> + UID erkannt: {activeNfc.uid} + + ) : ( + <>keine UID erkannt + )} +

+ {activeNfc.uid && + lastHardwareUid.uid === activeNfc.uid && + lastHardwareUid.hardwareUid && ( +

+ Hardware-UID erkannt: {lastHardwareUid.hardwareUid} +

+ )} + {!activeNfc.uid && activeNfc.hardwareUid && ( +

+ Hardware-UID erkannt: {activeNfc.hardwareUid} +

+ )} + {activeNfc.uid && tags.find((tag) => tag.uid === activeNfc.uid) && ( +

+ Dieser Tag ist bekannt, aber noch nicht zugewiesen. +

+ )} +
+ + +
+ { + if (event.index <= tagStepMax) { + setTagStep(event.index); + return; + } + tagStepperRef.current?.setActiveStep(tagStep); + }} + > + +
+
+ setScanTagUid(event.target.value)} + readOnly={tagUidMode === 'keep'} + placeholder="Tag-ID (10 Zeichen)" + className="p-inputtext-sm" + /> +
+ setScanTagLabel(event.target.value)} + placeholder="Label (optional)" + className="p-inputtext-sm is-hidden" + /> +
+
+ +
+
+ setScanMediaPath(event.value)} + options={collectTopLevelFolders(mediaTree).map((folderPath) => ({ + label: folderPath, + value: folderPath, + }))} + placeholder="Medienordner waehlen" + filter + className="p-inputtext-sm" + disabled={!tagStepOneDone} + /> + +
+
+
+ +
+
+

Zusammenfassung

+
+ System-UID + {scanTagUid || '-'} + UID-Modus + {tagUidMode === 'keep' ? 'Beibehalten' : 'Neu vergeben'} + Label + {scanTagLabel?.trim() || '-'} + Medienordner + {scanMediaPath || 'Spaeter zuordnen'} + Hardware-UID + + {lastHardwareUid.uid === activeNfc?.uid && lastHardwareUid.hardwareUid + ? lastHardwareUid.hardwareUid + : activeNfc?.hardwareUid || '-'} + +
+
+
+
+
+
+
+ {tags.some((tag) => tag.status === 'IMPORTED') && ( +
+ +
+ )} +
+
+ )} + {renderSection()} +
+ + +
+ + + + {activeModal === 'new-folder' && ( +
{ + setActiveModal(''); + setUploadAfterCreate(false); + setPendingUploadFiles([]); + setUploadSize(0); + setUploadNameError(''); + setTagDeleteTarget(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

{uploadAfterCreate ? 'Upload vorbereiten' : 'Neuen Ordner anlegen'}

+ { + setNewFolderName(event.target.value); + if (uploadNameError) setUploadNameError(''); + }} + placeholder={ + uploadAfterCreate && !currentPath + ? 'Ordnername' + : uploadAfterCreate + ? 'Optionaler Ordnername (leer = aktueller Ordner)' + : 'Ordnername' + } + className="p-inputtext-sm" + /> + {uploadAfterCreate && uploadNameError && ( +

{uploadNameError}

+ )} + {uploadAfterCreate && ( + <> + { + const audioFiles = (event.files || []).filter((file) => + file.type.startsWith('audio/') + ); + setPendingUploadFiles(audioFiles); + const nextSize = audioFiles.reduce((sum, file) => sum + file.size, 0); + setUploadSize(nextSize); + setActiveUploadLabel( + audioFiles.length + ? `Upload: ${audioFiles.length} Datei(en)` + : '' + ); + }} + uploadHandler={(event) => { + const audioFiles = (event.files || []).filter((file) => + file.type.startsWith('audio/') + ); + setPendingUploadFiles(audioFiles); + const nextSize = audioFiles.reduce((sum, file) => sum + file.size, 0); + setUploadSize(nextSize); + setActiveUploadLabel( + audioFiles.length + ? `Upload: ${audioFiles.length} Datei(en)` + : '' + ); + handleCreateFolder(audioFiles); + }} + onClear={() => { + setPendingUploadFiles([]); + setUploadSize(0); + }} + emptyTemplate={ +
+ + + Drag and Drop Audio Here + +
+ Gesamt: {formatSize(uploadSize)} +
+
+ } + /> + {pendingUploadFiles.length > 0 && ( +
+ Gesamt: {formatSize(uploadSize)} + + {pendingUploadFiles.length} Datei(en) + +
+ )} + + )} +
+
+
+
+ )} + + {activeModal === 'rename' && ( +
{ + setActiveModal(''); + setRenameName(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Umbenennen

+ setRenameName(event.target.value)} + placeholder="Neuer Name" + className="p-inputtext-sm" + /> +
+
+
+
+ )} + + {activeModal === 'delete' && ( +
{ + setActiveModal(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Eintraege loeschen

+

{selectedPaths.length} Eintraege werden geloescht.

+
+
+
+
+ )} + + {activeModal === 'move' && ( +
{ + setActiveModal(''); + setMoveTarget(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Verschieben

+ setMoveTarget(event.value)} + options={[ + { label: 'media/', value: '__root__' }, + ...collectFolderPaths(mediaTree).map((folder) => ({ + label: folder, + value: folder, + })), + ]} + placeholder="Zielordner waehlen" + filter + className="p-inputtext-sm" + /> +
+
+
+
+ )} + + {activeModal === 'tag-delete' && ( +
{ + setActiveModal(''); + setTagDeleteTarget(''); + }} + > +
event.stopPropagation()} + ref={modalRef} + > +

Tag entfernen

+

+ Soll der Tag komplett geloescht werden oder nur die Medienzuweisung? +

+
+
+
+
+ )} + + {boxDeleteTarget && ( +
setBoxDeleteTarget('')} + > +
event.stopPropagation()} + ref={modalRef} + > +

Box entfernen

+

Soll die Box wirklich entkoppelt werden?

+
+
+
+
+ )} + + {boxDetailsOpen && ( +
{ + setBoxDetailsOpen(false); + setBoxDetailsError(''); + setBoxDetailsData(null); + }} + role="presentation" + > +
event.stopPropagation()}> +
+

Box Details

+
+
+ {boxDetailsLoading &&

Details werden geladen...

} + {boxDetailsError &&

{boxDetailsError}

} + {!boxDetailsLoading && !boxDetailsError && boxDetailsData && ( +
+
+ Box +
ID: {boxDetailsData.box_id}
+
+ Firmware: {boxDetailsData.firmware_version || '0.0.0'} +
+
+ +
+ Settings +
+ Max Volume: {boxDetailsData.settings?.max_volume ?? '-'} +
+
+ Default Volume: {boxDetailsData.settings?.default_volume ?? '-'} +
+
+ +
+ Gespeicherte WLANs + {Array.isArray(boxDetailsData.wifi_profiles) && + boxDetailsData.wifi_profiles.length > 0 ? ( + boxDetailsData.wifi_profiles.map((profile) => ( +
+ {profile.ssid} (prio {profile.priority ?? 0}) +
+ )) + ) : ( +
Keine Profile.
+ )} +
+ +
+ Tags + {boxDetailsData.tags && Object.keys(boxDetailsData.tags).length > 0 ? ( + Object.entries(boxDetailsData.tags).map(([uid, tag]) => ( +
+ {uid}: {tag?.path || '-'} +
+ )) + ) : ( +
Keine Tags.
+ )} +
+ +
+ Server +
+ Active URL: {boxDetailsData.active_server_url || '-'} +
+
+ Last SSID: {boxDetailsData.last_ssid || '-'} +
+ {boxDetailsData.server_bindings && + Object.keys(boxDetailsData.server_bindings).length > 0 ? ( + Object.entries(boxDetailsData.server_bindings).map( + ([ssid, binding]) => ( +
+ {ssid}: {binding?.server_url || '-'} +
+ ) + ) + ) : ( +
Keine Bindings.
+ )} +
+
+ )} +
+
+
+ )} + + {activeModal === 'import-tag' && ( +
+
+
+

Tag vom Box-Speicher uebertragen

+
+
+ + setImportTargetFolder(event.target.value)} + placeholder="z. B. grimm_volume_3" + className="p-inputtext-sm" + /> + {uploadInProgress && ( +
+
+
+ )} +
+
+
+
+
+ )} + + {showSessionSheet && ( +
setShowSessionSheet(false)} + > +
event.stopPropagation()} + > +
+ ToolHub +
+ {drawerTab === 'boxes' && ( + <> + {paired.length === 0 && ( +

Keine gepairten Boxen.

+ )} +
+ {paired.map((box) => { + const isActive = selectedId === box.box_id; + const label = (box.alias || box.box_id || '').slice(0, 2).toUpperCase(); + const availability = getAvailability(box.last_seen); + return ( + + ); + })} +
+ + )} + {drawerTab === 'system' && ( +
{systemPanels}
+ )} +
+ + +
+
+
+ )} +
+ ); +} diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..38bf249 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,249 @@ +const defaultHost = + typeof window !== 'undefined' && window.location ? window.location.hostname : '127.0.0.1'; +const API_BASE = '/api'; + +async function readJson(response) { + const text = await response.text(); + try { + return { ok: response.ok, status: response.status, data: JSON.parse(text) }; + } catch (error) { + return { ok: response.ok, status: response.status, data: { detail: text } }; + } +} + +export async function getBoxes() { + const response = await fetch(`${API_BASE}/boxes`); + return readJson(response); +} + +export async function pairBox(boxId) { + const response = await fetch(`${API_BASE}/boxes/pair`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ box_id: boxId }), + }); + return readJson(response); +} + +export async function getStatus(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/status`); + return readJson(response); +} + +export async function getBoxDetails(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/details`); + return readJson(response); +} + +export async function getMediaTree() { + const response = await fetch(`${API_BASE}/media-tree`); + return readJson(response); +} + +export async function createMediaFolder(parentPath, name) { + const response = await fetch(`${API_BASE}/media/folder`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ parent_path: parentPath, name }), + }); + return readJson(response); +} + +export async function renameMedia(path, name) { + const response = await fetch(`${API_BASE}/media/rename`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, name }), + }); + return readJson(response); +} + +export async function moveMedia(path, targetParent) { + const response = await fetch(`${API_BASE}/media/move`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, target_parent: targetParent }), + }); + return readJson(response); +} + +export async function deleteMedia(path) { + const response = await fetch(`${API_BASE}/media/delete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + return readJson(response); +} + +export function uploadMedia(targetPath, files, onProgress) { + const formData = new FormData(); + formData.append('target_path', targetPath); + files.forEach((file) => { + formData.append('files', file); + }); + + return new Promise((resolve) => { + const request = new XMLHttpRequest(); + request.open('POST', `${API_BASE}/media/upload`); + request.upload.onprogress = (event) => { + if (!event.lengthComputable || !onProgress) return; + const percent = Math.round((event.loaded / event.total) * 100); + onProgress(percent); + }; + request.onload = () => { + try { + const data = JSON.parse(request.responseText || '{}'); + resolve({ ok: request.status >= 200 && request.status < 300, status: request.status, data }); + } catch (error) { + resolve({ + ok: request.status >= 200 && request.status < 300, + status: request.status, + data: { detail: request.responseText || '' }, + }); + } + }; + request.onerror = () => { + resolve({ ok: false, status: 0, data: { detail: 'network error' } }); + }; + request.send(formData); + }); +} + +export async function getTags() { + const response = await fetch(`${API_BASE}/tags`); + return readJson(response); +} + +export async function generateTag(label) { + const response = await fetch(`${API_BASE}/tags/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ label }), + }); + return readJson(response); +} + +export async function claimTag(uid, label) { + const response = await fetch(`${API_BASE}/tags/claim`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uid, label }), + }); + return readJson(response); +} + +export async function markTagWritten(uid) { + const response = await fetch(`${API_BASE}/tags/${uid}/write`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + return readJson(response); +} + +export async function getBoxTags(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/tags`); + return readJson(response); +} + +export async function getBoxLocalTags(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/local-tags`); + return readJson(response); +} + +export async function getBoxStorage(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/storage`); + return readJson(response); +} + +export async function getTagBlocks(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/tag-blocks`); + return readJson(response); +} + +export async function setTagBlock(boxId, uid, blocked) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/tag-blocks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uid, blocked }), + }); + return readJson(response); +} + +export async function setBoxAlias(boxId, alias) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/alias`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ alias }), + }); + return readJson(response); +} + +export async function setTagAlias(uid, alias) { + const response = await fetch(`${API_BASE}/tags/${uid}/alias`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ alias }), + }); + return readJson(response); +} + +export async function assignTag(uid, boxId) { + const response = await fetch(`${API_BASE}/tags/${uid}/assign`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ box_id: boxId }), + }); + return readJson(response); +} + +export async function unassignTag(uid, boxId) { + const response = await fetch(`${API_BASE}/tags/${uid}/unassign`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ box_id: boxId }), + }); + return readJson(response); +} + +export async function deleteTag(uid) { + const response = await fetch(`${API_BASE}/tags/${uid}`, { + method: 'DELETE', + }); + return readJson(response); +} + +export async function setTagMedia(uid, mediaPath) { + const response = await fetch(`${API_BASE}/tags/${uid}/media`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ media_path: mediaPath }), + }); + return readJson(response); +} + +export async function pullTagFromBox(boxId, uid, targetFolder) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/pull-tag`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uid, target_folder: targetFolder }), + }); + return readJson(response); +} + +export async function sendCommand(boxId, command, payload = {}) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/command`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command, payload }), + }); + return readJson(response); +} + +export async function unpairBox(boxId) { + const response = await fetch(`${API_BASE}/boxes/${boxId}/unpair`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + return readJson(response); +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..7057e78 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { PrimeReactProvider } from 'primereact/api'; +import 'primereact/resources/themes/lara-dark-teal/theme.css'; +import 'primereact/resources/primereact.min.css'; +import 'primeicons/primeicons.css'; +import App from './App.jsx'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + +); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..c70b8d8 --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,1933 @@ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); + +:root { + color-scheme: dark; + font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif; + background-color: #0b0f14; + color: #e6edf6; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at 20% 10%, rgba(50, 112, 124, 0.35), transparent 45%), + radial-gradient(circle at 80% 0%, rgba(24, 45, 67, 0.6), transparent 40%), + linear-gradient(130deg, #0b0f14 0%, #111826 45%, #0d1220 100%); +} + +#root { + min-height: 100vh; +} + +.app-shell { + min-height: 100vh; + padding-bottom: 80px; +} + +.topbar { + position: sticky; + top: 0; + z-index: 10; + display: flex; + gap: 16px; + align-items: center; + padding: 18px 24px; + background: rgba(11, 15, 20, 0.92); + backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 12px; + color: rgba(230, 237, 246, 0.7); +} + +.brand strong { + display: block; + font-size: 14px; + color: #e6edf6; +} + +.brand span { + display: block; + margin-top: 2px; +} + +.brand-mark { + width: 36px; + height: 36px; + border-radius: 12px; + display: grid; + place-items: center; + background: linear-gradient(130deg, #19a4a1, #0b6f84); + color: #061018; + font-weight: 700; +} + + +.topbar-actions { + display: flex; + align-items: center; + gap: 12px; + margin-left: auto; +} + +.header-box-indicator { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 999px; + background: rgba(25, 164, 161, 0.18); + border: 1px solid rgba(25, 164, 161, 0.4); + color: rgba(230, 237, 246, 0.9); + font-size: 13px; + margin-left: auto; +} + +.user-avatar { + background: #1c2735; + color: #e6edf6; +} + +.layout { + display: grid; + grid-template-columns: 240px minmax(0, 1fr) 280px; + gap: 24px; + padding: 24px; +} + +.sidebar, +.right-panel { + position: sticky; + top: 96px; + align-self: start; + height: fit-content; +} + +.sidebar { + padding: 16px; + border-radius: 20px; + background: rgba(10, 17, 26, 0.85); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.sidebar-title { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; + color: rgba(230, 237, 246, 0.55); + margin: 0 0 12px; +} + +.sidebar-section { + margin-bottom: 16px; +} + +.nav-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.nav-button { + justify-content: flex-start; + color: rgba(230, 237, 246, 0.7); +} + +.nav-button.active { + background: rgba(25, 164, 161, 0.15); + color: #e6edf6; + border-radius: 12px; +} + +.button-stack { + display: flex; + flex-direction: column; + gap: 8px; +} + +.session-card { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 12px; + align-items: center; + padding: 12px; + border-radius: 14px; + background: rgba(10, 17, 26, 0.7); + border: 1px solid rgba(255, 255, 255, 0.08); + text-align: left; +} + +.sheet-card .session-card { + width: 100%; + height: 72px; +} + +.session-card strong { + color: #2dd4bf; +} + +.session-card .meta { + display: block; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.session-list { + display: grid; + gap: 8px; +} + +.sheet-card .session-list { + grid-template-columns: repeat(3, minmax(0, 1fr)); + align-items: stretch; +} + +.session-card.active { + border-color: rgba(25, 164, 161, 0.6); + box-shadow: 0 0 0 2px rgba(25, 164, 161, 0.15); +} + +.session-card:focus-visible { + outline: 2px solid rgba(25, 164, 161, 0.6); + outline-offset: 2px; +} + +.content { + display: flex; + flex-direction: column; + gap: 20px; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.section-header h1 { + margin: 0 0 4px; + font-size: 28px; +} + +.section-header p { + margin: 0; + color: rgba(230, 237, 246, 0.6); +} + +.section-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.section-stack { + display: flex; + flex-direction: column; + gap: 20px; +} + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +.stat-card { + background: rgba(14, 24, 35, 0.9); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.stat-label { + margin: 0; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.stat-value { + font-size: 24px; + font-weight: 600; + margin: 8px 0; +} + +.stat-helper { + font-size: 12px; + color: rgba(25, 164, 161, 0.9); +} + +.split-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 16px; +} + +.wide-card, +.content-card, +.panel-card { + background: rgba(14, 24, 35, 0.92); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.card-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.progress-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 12px; + align-items: center; + margin-top: 12px; + font-size: 12px; + color: rgba(230, 237, 246, 0.7); +} + +.activity-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; +} + +.activity-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + font-size: 13px; +} + +.activity-item span { + display: block; + font-size: 11px; + color: rgba(230, 237, 246, 0.55); +} + +.content-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.content-card h3 { + margin-top: 0; +} + +.placeholder-block { + height: 120px; + border-radius: 14px; + background: linear-gradient(130deg, rgba(25, 164, 161, 0.2), rgba(23, 45, 64, 0.9)); +} + +.placeholder-table { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; +} + +.table-row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 12px; + align-items: center; + padding: 8px 12px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); + font-size: 13px; +} + +.table-row.header { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(230, 237, 246, 0.5); +} + +.placeholder-tree { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; + font-size: 13px; + color: rgba(230, 237, 246, 0.75); +} + +.placeholder-tree .nested { + margin-left: 16px; +} + +.queue-item { + display: grid; + gap: 8px; + margin-top: 12px; + padding: 10px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + font-size: 13px; +} + +.queue-item span { + display: block; + font-size: 11px; + color: rgba(230, 237, 246, 0.5); +} + +.tab-shell .p-tabview-nav { + background: transparent; + border-color: rgba(255, 255, 255, 0.08); +} + +.tag-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-top: 12px; +} + +.tag-card { + background: rgba(255, 255, 255, 0.03); +} + +.tag-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.settings-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; +} + +.token-chip { + display: inline-flex; + align-items: center; + padding: 6px 12px; + border-radius: 999px; + margin-right: 8px; + margin-top: 8px; + background: rgba(25, 164, 161, 0.18); + font-size: 12px; +} + +.right-panel { + display: flex; + flex-direction: column; + gap: 16px; +} + +.panel-card h3 { + margin-top: 0; +} + +.status-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; + font-size: 13px; +} + +.summary { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1 1 100%; +} + +.summary-title { + font-size: 14px; + font-weight: 600; + color: rgba(230, 237, 246, 0.95); + margin: 0; +} + +.summary-grid { + display: grid; + grid-template-columns: 140px 1fr; + column-gap: 12px; + row-gap: 6px; + font-size: 13px; +} + +.summary-grid span { + color: var(--text-color-secondary); + font-size: 11px; +} + +.step-actions { + width: 100%; + display: flex; + justify-content: flex-end; +} + +.step-row { + width: 100%; + display: flex; + align-items: center; + gap: 8px; +} + +.step-row .p-inputtext, +.step-row .p-dropdown { + flex: 1 1 220px; + min-width: 0; +} + +.step-row-spacer { + flex: 1 1 auto; +} + +.step-messages { + display: grid; + gap: 6px; + margin-top: 6px; +} + +.p-messages { + margin: 5px 0; +} + +.p-message { + min-height: 30px; + max-height: 30px; + padding: 4px 8px; + font-size: 12px; + overflow: hidden; + display: flex; + align-items: center; +} + +.p-message-summary { + font-size: 12px; + line-height: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-flex; + align-items: center; +} + +.p-message-detail { + font-size: 11px; + line-height: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-flex; + align-items: center; +} + +.p-message-wrapper { + gap: 6px; + align-items: center; + flex-wrap: nowrap; + overflow: hidden; + min-width: 0; + flex: 1 1 auto; +} + +.p-message-text { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.is-hidden { + display: none; +} + +.panel-pill { + padding: 10px 12px; + border-radius: 12px; + margin-top: 8px; + background: rgba(255, 255, 255, 0.04); + font-size: 13px; +} + +.capacity-row { + display: grid; + gap: 8px; + margin-top: 10px; +} + +.capacity-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-size: 13px; + color: rgba(230, 237, 246, 0.9); +} + +.capacity-meta { + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.capacity-bar .p-metergroup-meter { + background: linear-gradient( + 90deg, + #39d98a 0%, + #e8d45a 45%, + #f2a94f 65%, + #b68bff 100% + ); + background-size: calc(var(--capacity-scale, 1) * 100%) 100%; + background-position: left center; +} + +.capacity-bar .p-metergroup-labels, +.capacity-bar .p-metergroup-label, +.capacity-bar .p-metergroup-label-type { + display: none; +} + +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: none; + gap: 6px; + padding: 10px 12px 16px; + background: rgba(11, 15, 20, 0.95); + border-top: 1px solid rgba(255, 255, 255, 0.08); + z-index: 20; + grid-template-columns: repeat(6, minmax(0, 1fr)); +} + +.bottom-nav .p-button { + width: 100%; + min-width: 0; + display: grid; + place-items: center; /* bleibt */ + gap: 4px; + background: transparent; + border: none; + color: rgba(230, 237, 246, 0.6); + font-size: 11px; +} + +.bottom-nav .p-button > span { + display: flex; + align-items: center; + justify-content: center; +} + + + +.bottom-nav .p-button.active { + color: #19a4a1; +} + +@media (max-width: 550px) { + /* ❌ nur Text ausblenden */ + .bottom-nav .p-button > span:not(.nav-icon) { + display: none; + } + + /* ✅ Icon bleibt exakt zentriert */ + .bottom-nav .nav-icon { + display: flex; + align-items: center; + justify-content: center; + } +} + + + +@media (max-width: 1100px) { + .layout { + grid-template-columns: 220px minmax(0, 1fr); + } + + .right-panel { + display: none; + } +} + +@media (max-width: 1300px) { + .legacy .boxes-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 980px) { + .legacy .boxes-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 1200px) { + .split-grid { + grid-template-columns: 1fr; + } + + .legacy .boxes-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .topbar-actions { + display: none; + } + + .legacy .boxes-grid { + grid-template-columns: 1fr; + } + + .legacy .boxes-grid .card { + flex-direction: column; + align-items: stretch; + } + + .legacy .boxes-grid .box-actions-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + width: 100%; + } + + .legacy .boxes-grid .box-actions-row > * { + width: 100%; + } + + .sheet-card .session-list { + grid-template-columns: 1fr; + } +} + +@media (max-width: 760px) { + .layout { + grid-template-columns: minmax(0, 1fr); + padding: 16px; + } + + .sidebar { + display: none; + } + + + .section-header { + flex-direction: column; + align-items: flex-start; + } + + .bottom-nav { + display: grid; + grid-template-columns: repeat(6, 1fr); + } +} + +@media (max-width: 720px) { + .sheet-card .session-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 520px) { + .sheet-card .session-list { + grid-template-columns: 1fr; + } +} + +.sheet-backdrop { + position: fixed; + inset: 0; + background: rgba(5, 8, 12, 0.6); + display: grid; + align-items: end; + z-index: 50; +} + +.sheet-card { + background: #0f1622; + border-radius: 20px 20px 0 0; + padding: 20px 18px 28px; + border: 1px solid rgba(255, 255, 255, 0.08); + max-height: 80vh; + overflow-y: auto; +} + +.drawer-system { + display: grid; + gap: 12px; +} + +.drawer-tabs { + margin: 12px -18px -28px; + padding: 10px 12px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(11, 15, 20, 0.95); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.drawer-tab { + background: transparent; + border: none; + border-radius: 12px; + color: rgba(230, 237, 246, 0.6); + padding: 8px 10px; + font-size: 11px; + display: grid; + place-items: center; + gap: 4px; + width: 100%; + min-width: 0; + cursor: pointer; +} + +.drawer-tab .nav-icon { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.drawer-tab.active { + color: #19a4a1; +} + +.drawer-system { + display: grid; + gap: 12px; +} + +.sheet-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + + +.error { + background: rgba(80, 24, 32, 0.6); + color: #f4a7a7; + border: 1px solid rgba(242, 201, 201, 0.4); + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 16px; +} + +.muted { + color: rgba(230, 237, 246, 0.6); +} + +.status { + background: #0f1520; + color: #e6edf6; + padding: 12px; + border-radius: 12px; + font-size: 12px; + overflow: auto; + border: 1px solid rgba(255, 255, 255, 0.08); + max-width: 100%; + white-space: pre-wrap; + word-break: break-word; +} + +.legacy .card { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 12px; + margin-top: 12px; + cursor: pointer; + background: rgba(10, 17, 26, 0.7); +} + +.legacy .card.selected { + border-color: rgba(25, 164, 161, 0.6); + box-shadow: 0 0 0 2px rgba(25, 164, 161, 0.15); +} + +.legacy .card.compact { + padding: 10px; + margin-top: 10px; +} + +.legacy .tags-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +@media (max-width: 1800px) { + .legacy .tags-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 1500px) { + .legacy .tags-grid { + grid-template-columns: 1fr; + } +} + +.legacy .boxes-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.legacy .boxes-grid .card { + margin-top: 0; +} + +@media (max-width: 1300px) { + .legacy .tags-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 1200px) { + .legacy .tags-grid { + grid-template-columns: 1fr; + } + + .legacy .boxes-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .legacy .tags-grid { + grid-template-columns: 1fr; + } + + .legacy .boxes-grid { + grid-template-columns: 1fr; + } +} + +.legacy .stack { + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-end; +} + +.legacy .stack-inline { + flex-direction: row; + align-items: center; + justify-content: flex-start; + flex-wrap: nowrap; + gap: 6px; +} + +.legacy .tag-actions { + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +.legacy .tag-actions-row { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + justify-content: flex-end; +} + +.legacy .box-actions { + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +.legacy .box-actions-row { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + justify-content: flex-end; +} + +.legacy .box-actions .alias-input { + width: 100%; + max-width: none; +} + +@media (max-width: 1800px) { + .legacy .boxes-grid .card { + flex-direction: column; + align-items: stretch; + } + + .legacy .boxes-grid .box-actions-row { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 8px; + width: 100%; + } + + .legacy .boxes-grid .box-actions-row > * { + width: 100%; + } + + .legacy .tags-grid .card { + flex-direction: column; + align-items: stretch; + } + + .legacy .tags-grid .tag-actions-row { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 8px; + width: 100%; + } + + .legacy .tags-grid .tag-actions-row > * { + width: 100%; + } +} + +.legacy .tag-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + flex-wrap: nowrap; +} + +.legacy .tags-grid .tag-row { + display: grid; + grid-template-columns: 1fr 2fr; + align-items: center; + gap: 12px; +} + +.legacy .tags-grid .tag-actions { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; +} + +.legacy .tags-grid .tag-actions-row { + width: 100%; + display: grid; + grid-template-columns: 2fr 1fr; + align-items: center; + gap: 8px; +} + +.legacy .tags-grid .tag-actions-row .alias-input, +.legacy .tags-grid .tag-actions-row .p-dropdown { + width: 100%; + min-width: 0; +} + +.legacy .tags-grid .tag-actions-row .icon-button { + width: 100%; +} + +.upload-item-meta { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + width: 100%; +} + +.upload-item-name { + text-align: left; + justify-self: start; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.upload-item-size { + text-align: right; + white-space: nowrap; +} + +.upload-item-footer { + width: 100%; + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 8px; + color: rgba(230, 237, 246, 0.7); + font-size: 12px; +} + +.upload-item-total { + text-align: right; +} + +.upload-item-count { + text-align: right; +} + +@media (max-width: 600px) { + .legacy .tags-grid .tag-row { + grid-template-columns: 1fr; + } + + .legacy .boxes-grid .card { + flex-direction: column; + align-items: stretch; + } + + .legacy .tags-grid .tag-actions-row { + grid-template-columns: 2fr 1fr; + } + + .legacy .boxes-grid .box-actions-row { + grid-template-columns: 2fr 1fr 1fr; + } +} + +.legacy .box-tag-row { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + margin-top: 12px; + background: rgba(10, 17, 26, 0.7); +} + +.legacy .box-tag-actions { + display: flex; + align-items: flex-start; +} + +.legacy .file-list { + margin: 8px 0 0; + padding-left: 18px; + color: rgba(230, 237, 246, 0.7); + font-size: 13px; +} + +.legacy .file-list li { + margin-bottom: 4px; +} + +.legacy .tag-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.legacy .box-title { + display: flex; + align-items: center; + gap: 8px; +} + +.legacy .alias-input:not(.p-inputtext), +.legacy input:not(.p-inputtext), +.modal-card input:not(.p-inputtext) { + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + padding: 6px 8px; + font-size: 12px; + min-width: 140px; + background: rgba(255, 255, 255, 0.04); + color: #e6edf6; +} + +.legacy select, +.modal-card select { + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + padding: 8px 10px; + min-width: 180px; + background: rgba(255, 255, 255, 0.04); + color: #e6edf6; +} + +.legacy .tag-matrix { + display: grid; + gap: 8px; + overflow-x: auto; +} + +.legacy .matrix-row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(120px, 1fr); + gap: 8px; + align-items: center; +} + +.legacy .matrix-row.header { + font-weight: 600; + color: rgba(230, 237, 246, 0.8); +} + +.legacy .matrix-cell { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); + font-size: 12px; +} + +.legacy .matrix-cell.toggle { + justify-content: center; + cursor: pointer; + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.02); + transition: background 0.2s ease, border-color 0.2s ease; +} + +.legacy .matrix-cell.toggle.is-blocked { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(239, 68, 68, 0.08); +} + +.legacy .matrix-cell.toggle.p-message { + width: 100%; + justify-content: center; + background: transparent; + border: 0; + padding: 0; +} + +.legacy .matrix-cell.toggle.p-message .p-message-text { + justify-content: center; + width: 100%; +} + +.legacy .matrix-cell.label { + font-weight: 600; + background: rgba(10, 17, 26, 0.8); +} + +.legacy .meta { + font-size: 12px; + color: rgba(230, 237, 246, 0.55); +} + +.legacy .pill { + background: rgba(25, 164, 161, 0.2); + color: #8fe3df; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; +} + +.legacy .controls { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} + +.legacy .controls-inline { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.legacy button:not(.p-button), +.modal-card button:not(.p-button) { + border: 1px solid rgba(255, 255, 255, 0.1); + background: #1b2736; + color: #e6edf6; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; +} + +.legacy button:not(.p-button):hover, +.modal-card button:not(.p-button):hover { + background: #233144; +} + +.legacy .button-ghost, +.modal-card .button-ghost { + background: transparent; + color: rgba(230, 237, 246, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.legacy .button-ghost:hover, +.modal-card .button-ghost:hover { + background: rgba(255, 255, 255, 0.06); +} + +.icon-button { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(10, 17, 26, 0.7); + color: #e6edf6; + padding: 6px 8px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.icon-button:hover { + background: rgba(255, 255, 255, 0.08); +} + +.icon-button.danger { + border-color: rgba(239, 68, 68, 0.55); + color: rgba(248, 113, 113, 0.95); + background: rgba(239, 68, 68, 0.12); +} + +.icon-button.danger:hover { + background: rgba(239, 68, 68, 0.18); +} + +.icon-button.upload { + position: relative; + overflow: hidden; +} + +.icon-button:disabled { + cursor: not-allowed; + opacity: 0.4; +} + +.modal-card .danger { + border: 1px solid rgba(239, 68, 68, 0.55); + background: rgba(239, 68, 68, 0.12); + color: rgba(248, 113, 113, 0.95); +} + +.modal-card .danger:hover { + background: rgba(239, 68, 68, 0.18); +} + +.details-stack > * + * { + margin-top: 12px; +} + +.icon-button.upload input { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.explorer { + display: grid; + grid-template-columns: 260px 1fr; + grid-template-rows: 1fr auto; + gap: 0; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + overflow-x: auto; + overflow-y: hidden; + background: rgba(8, 12, 18, 0.7); + min-height: 200px; + align-items: stretch; + font-size: 13px; +} + +.explorer-sidebar { + grid-row: 1; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; + background: rgba(10, 17, 26, 0.85); + height: 100%; +} + +.sidebar-toolbar { + display: flex; + align-items: center; + justify-content: flex-start; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + padding: 0 10px; + min-height: 40px; + gap: 10px; +} + +.sidebar-tree { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(10, 17, 26, 0.8); + padding: 8px; + overflow: auto; +} + +.sidebar-search { + flex: 1; + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.04); + border-radius: 8px; + padding: 0 10px; + font-size: 13px; + color: #e6edf6; + min-width: 0; + height: 28px; + line-height: 28px; +} + +.sidebar-search:focus { + outline: none; + border-color: rgba(25, 164, 161, 0.6); + box-shadow: 0 0 0 2px rgba(25, 164, 161, 0.2); +} + +.explorer-toolbar, +.sidebar-toolbar { + height: 40px; + min-height: 40px; + box-shadow: 0 1px 2px rgba(16, 18, 22, 0.2); + align-items: center; +} + +.explorer-actions.footer { + padding-top: 6px; +} + +.explorer-main { + grid-row: 1; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px 14px; + background: rgba(10, 17, 26, 0.7); + height: 100%; +} + +.explorer-toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 0 10px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); +} + +.explorer-toolbar .icon-button, +.explorer-toolbar label.icon-button { + width: 30px; + height: 30px; + padding: 0; +} + +.explorer-toolbar .icon-button svg, +.explorer-toolbar label.icon-button svg { + display: block; +} + +.explorer-path { + display: flex; + flex-wrap: wrap; + gap: 6px; + font-size: 13px; + color: rgba(230, 237, 246, 0.7); + align-items: center; + line-height: 1; +} + +.breadcrumb-root { + font-weight: 600; + color: #e6edf6; +} + +.path-link { + background: transparent; + border: none; + color: #e6edf6; + cursor: pointer; + padding: 0; + font-size: 13px; + font-weight: 600; +} + +.path-link:hover, +.path-link:focus { + background: transparent; + color: #e6edf6; + outline: none; +} + +.toolbar-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 6px; +} + +.explorer-list { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: rgba(10, 17, 26, 0.8); + overflow: hidden; +} + +.explorer-row { + display: grid; + grid-template-columns: 2fr repeat(2, minmax(80px, 1fr)); + gap: 12px; + align-items: center; + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + font-size: 13px; +} + +.explorer-row.has-meta, +.explorer-list.has-meta .explorer-row { + grid-template-columns: 2fr repeat(4, minmax(100px, 1fr)); +} + +.explorer-row.header { + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(230, 237, 246, 0.5); + background: rgba(255, 255, 255, 0.02); +} + +.explorer-row.footer { + background: rgba(255, 255, 255, 0.02); +} + +.explorer-row.selected { + background: rgba(25, 164, 161, 0.15); +} + +.row-name { + display: flex; + align-items: center; + gap: 8px; +} + +.row-icon { + display: inline-flex; + align-items: center; +} + +.footer-count { + font-weight: 600; + color: rgba(230, 237, 246, 0.9); +} + +.explorer-footer { + grid-column: 1 / -1; + padding: 8px 12px; + font-size: 13px; + color: rgba(230, 237, 246, 0.6); + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(10, 17, 26, 0.8); +} + +.tree-node { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tree-label { + display: inline-block; + user-select: none; + cursor: pointer; +} + +.depth-1 .tree-row { + margin-left: 16px; +} + +.depth-2 .tree-row { + margin-left: 32px; +} + +.depth-3 .tree-row { + margin-left: 48px; +} + +.depth-4 .tree-row { + margin-left: 64px; +} + +.depth-5 .tree-row { + margin-left: 80px; +} + +.tree-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + border-radius: 8px; + cursor: pointer; + color: rgba(230, 237, 246, 0.75); +} + +.tree-row.active { + background: rgba(25, 164, 161, 0.18); + color: #e6edf6; +} + +.tree-caret { + border: none; + background: transparent; + color: rgba(230, 237, 246, 0.7); + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + flex: 0 0 18px; + cursor: pointer; +} + +.tree-caret.disabled { + width: 18px; + flex: 0 0 18px; +} + +.tree-icon.folder { + color: rgba(25, 164, 161, 0.9); +} + +.tree-label { + flex: 1; +} + +.tree-tag-count { + background: rgba(25, 164, 161, 0.2); + padding: 2px 6px; + border-radius: 999px; + font-size: 11px; +} + +.upload-status { + display: grid; + gap: 8px; + padding: 10px 12px; + margin-bottom: 12px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.04); +} + +.upload-progress { + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 999px; + overflow: hidden; +} + +.upload-progress div { + height: 100%; + background: linear-gradient(120deg, #19a4a1, #11709c); +} + + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(5, 8, 12, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 90; + backdrop-filter: blur(6px); +} + +.modal-card { + background: #0f1622; + color: #e6edf6; + border-radius: 14px; + padding: 20px; + width: min(520px, 92vw); + border: 1px solid rgba(255, 255, 255, 0.08); + display: grid; + gap: 12px; + position: relative; + z-index: 91; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); + max-height: 90vh; + overflow: auto; + visibility: visible; + opacity: 1; +} + +.modal-card.modal-upload { + width: min(1040px, 96vw); +} + +.modal-card.modal-tag-delete { + width: min(760px, 96vw); + overflow: visible; +} + + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 6px; + flex-wrap: nowrap; +} + +.modal-header h3 { + margin: 0; +} + +.modal-body { + display: grid; + gap: 10px; +} + +.modal-label { + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +@media (max-width: 960px) { + .explorer { + grid-template-columns: 1fr; + } + + .explorer-sidebar { + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + } +} + +.player-shell { + display: grid; + gap: 16px; +} + +.player-header { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 16px; + align-items: center; + padding: 14px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.04); +} + +.player-cover { + width: 86px; + height: 86px; + border-radius: 16px; + background: linear-gradient(135deg, #ff7a18, #af002d 60%, #319197); + box-shadow: 0 10px 20px rgba(5, 8, 12, 0.4); +} + +.player-meta h3 { + margin: 0 0 6px; + font-size: 20px; +} + +.player-meta p { + margin: 0 0 6px; + color: rgba(230, 237, 246, 0.6); + font-size: 12px; +} + +.player-sub { + display: flex; + gap: 12px; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.player-controls { + display: grid; + grid-auto-flow: column; + gap: 8px; + align-items: center; + justify-content: end; + background: rgba(255, 255, 255, 0.06); + padding: 10px; + border-radius: 14px; +} + +.player-btn { + border: none; + background: transparent; + color: rgba(230, 237, 246, 0.8); + width: 36px; + height: 36px; + border-radius: 50%; + display: grid; + place-items: center; + cursor: pointer; +} + +.player-btn.primary { + background: rgba(230, 237, 246, 0.12); + color: #e6edf6; + width: 42px; + height: 42px; +} + +.player-btn:hover { + background: rgba(255, 255, 255, 0.12); +} + +.volume-wrap { + position: relative; +} + +.volume-popover { + position: absolute; + right: 0; + bottom: 46px; + background: rgba(10, 17, 26, 0.95); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 12px; + padding: 10px 12px; + display: grid; + gap: 8px; + align-items: center; + justify-items: center; + z-index: 5; +} + +.volume-label { + font-size: 11px; + color: rgba(230, 237, 246, 0.7); + min-width: 2ch; + text-align: center; +} + +.player-progress { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 12px; + align-items: center; + font-size: 12px; + color: rgba(230, 237, 246, 0.6); +} + +.player-nfc { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.player-nfc input { + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + padding: 8px 10px; + background: rgba(255, 255, 255, 0.04); + color: #e6edf6; +} + +.player-nfc button { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(10, 17, 26, 0.7); + color: #e6edf6; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; +} + +.player-list { + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + overflow: hidden; +} + +.player-row { + display: grid; + grid-template-columns: 40px 2fr 1fr 80px; + gap: 12px; + align-items: center; + padding: 10px 12px; + font-size: 13px; + background: rgba(10, 17, 26, 0.65); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + cursor: pointer; + user-select: none; +} + +.player-row.header { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; + color: rgba(230, 237, 246, 0.5); + background: rgba(255, 255, 255, 0.03); +} + +.player-row.empty { + color: rgba(230, 237, 246, 0.6); +} + +.player-row.active { + background: rgb(45 212 191 / 12%); + color: #f6d08c; +} + +.player-row.active span { + color: inherit; +} + +@media (max-width: 900px) { + .player-header { + grid-template-columns: 1fr; + justify-items: start; + } + + .player-controls { + width: 100%; + justify-content: start; + } +} +.modal-long-button .p-button-label { + white-space: nowrap; +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..ee322b1 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,23 @@ +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + const rawTarget = env.VITE_BACKEND_URL || 'http://127.0.0.1:5001'; + const apiTarget = rawTarget.replace(/\/api\/?$/, ''); + + return { + plugins: [react()], + server: { + host: true, + port: 5174, + strictPort: true, + proxy: { + '/api': { + target: apiTarget, + changeOrigin: true, + }, + }, + }, + }; +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..9c21c4c --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Mediathek Loader + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b589c94 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1681 @@ +{ + "name": "mediathedownloader", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mediathedownloader", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "primeflex": "^4.0.0", + "primeicons": "^7.0.0", + "primereact": "^10.9.7", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.7.0", + "vite": "^5.4.19" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true + }, + "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, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/primeflex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-4.0.0.tgz", + "integrity": "sha512-UOEZCRjR36+sm5bUpDhS1xbA068l9VC6y1aTNVqQPtXuKIdPTqAWHRUxj3mKAoPrQ9W373ooJJMgNVXfiaw04g==" + }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==" + }, + "node_modules/primereact": { + "version": "10.9.7", + "resolved": "https://registry.npmjs.org/primereact/-/primereact-10.9.7.tgz", + "integrity": "sha512-Ap/lg9GGaS8Pq7IIlzguuG3qlaU6PYF6E0cCRo0rnWauRw/SQGvfreSVIIxqEhtR6xqlf7OV759lyvVOvBzmsQ==", + "dependencies": { + "@types/react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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/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-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-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.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": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.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": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..58e15c4 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "mediathedownloader", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "start": "python3 app.py" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@vitejs/plugin-react": "^4.7.0", + "vite": "^5.4.19" + }, + "dependencies": { + "primeflex": "^4.0.0", + "primeicons": "^7.0.0", + "primereact": "^10.9.7", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..81f942b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask +requests +cryptography diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..66a902b --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,1296 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Button } from 'primereact/button'; +import { Card } from 'primereact/card'; +import { Checkbox } from 'primereact/checkbox'; +import { DataView } from 'primereact/dataview'; +import { Dialog } from 'primereact/dialog'; +import { Divider } from 'primereact/divider'; +import { Dropdown } from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { MultiSelect } from 'primereact/multiselect'; +import { Password } from 'primereact/password'; +import { Tag } from 'primereact/tag'; +import { Toast } from 'primereact/toast'; + +const STORAGE_KEY = 'arte-loader-selection'; +const DEFAULT_ROWS = 8; +const ROW_OPTIONS = [5, 8, 10, 20, 50, 100]; + +function loadSelection() { + try { + return JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '{}'); + } catch { + return {}; + } +} + +function formatDuration(seconds) { + if (!seconds) return '-'; + return `${Math.floor(seconds / 60)} min`; +} + +function formatDate(timestamp) { + if (!timestamp) return '-'; + return new Intl.DateTimeFormat('de-DE').format(new Date(timestamp * 1000)); +} + +function formatSize(bytes) { + if (!bytes) return '-'; + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`; +} + +function normalizePositiveSize(value) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) { + return 0; + } + return parsed; +} + +function normalizeQualities(qualities, fallbackUrl) { + const list = []; + const seen = new Set(); + + if (Array.isArray(qualities)) { + qualities.forEach((q, index) => { + if (!q || typeof q !== 'object') return; + const url = (q.url || '').trim(); + if (!url || seen.has(url)) return; + seen.add(url); + const id = (q.id || `q${index + 1}`).toString(); + list.push({ + id, + label: q.label || id, + url, + size: normalizePositiveSize(q.size) + }); + }); + } + + if (!list.length && fallbackUrl) { + list.push({ + id: 'standard', + label: 'Standard', + url: fallbackUrl, + size: 0 + }); + } + + return list; +} + +function pickSelectedQuality(qualities, preferred) { + if (!qualities.length) return ''; + if (preferred && qualities.some((q) => q.id === preferred)) { + return preferred; + } + return qualities[0].id; +} + +function getSelectedVideoUrl(entry) { + if (!entry) return ''; + + const qualities = Array.isArray(entry.qualities) ? entry.qualities : []; + const selected = qualities.find((q) => q.id === entry.selectedQuality); + if (selected?.url) return selected.url; + + if (qualities[0]?.url) return qualities[0].url; + return (entry.videoUrl || '').trim(); +} + +function getQualitySizeForItem(item, qualityId, qualitySizeCache = {}) { + const fallbackSize = normalizePositiveSize(item?.size); + const qualities = Array.isArray(item?.qualities) ? item.qualities : []; + + const selected = qualities.find((quality) => quality.id === qualityId) || qualities[0]; + if (!selected) { + return fallbackSize; + } + + const directSize = normalizePositiveSize(selected.size); + if (directSize > 0) { + return directSize; + } + + const cachedSize = normalizePositiveSize(qualitySizeCache[selected.url]); + if (cachedSize > 0) { + return cachedSize; + } + + return fallbackSize; +} + +function buildSelectionEntryFromResult(item, existing = {}, preferredQuality = '') { + const qualities = normalizeQualities( + existing.qualities?.length ? existing.qualities : item.qualities, + existing.videoUrl || '' + ); + const selectedQuality = pickSelectedQuality( + qualities, + preferredQuality || existing.selectedQuality || item.default_quality + ); + + return { + video: Boolean(existing.video), + sub: Boolean(existing.sub), + title: existing.title || item.title || 'Unbekannte Sendung', + channel: existing.channel || item.channel || 'Unbekannter Sender', + timestamp: Number(existing.timestamp) || Number(item.timestamp) || 0, + subtitleUrl: existing.subtitleUrl || item.subtitle || '', + qualities, + selectedQuality, + videoUrl: getSelectedVideoUrl({ qualities, selectedQuality, videoUrl: existing.videoUrl }) + }; +} + +function normalizeSelectionMap(rawValue) { + if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) { + return {}; + } + + const normalized = {}; + + Object.entries(rawValue).forEach(([id, value]) => { + if (!value || typeof value !== 'object') return; + + const qualities = normalizeQualities(value.qualities, value.videoUrl || ''); + const selectedQuality = pickSelectedQuality(qualities, value.selectedQuality); + + const entry = { + video: Boolean(value.video), + sub: Boolean(value.sub), + title: value.title || '', + channel: value.channel || '', + timestamp: Number(value.timestamp) || 0, + subtitleUrl: value.subtitleUrl || '', + qualities, + selectedQuality, + videoUrl: getSelectedVideoUrl({ qualities, selectedQuality, videoUrl: value.videoUrl || '' }) + }; + + if (entry.video || entry.sub) { + normalized[id] = entry; + } + }); + + return normalized; +} + +function App() { + const toast = useRef(null); + const [query, setQuery] = useState(''); + const [channels, setChannels] = useState([]); + const [selectedChannels, setSelectedChannels] = useState([]); + const [results, setResults] = useState([]); + const [selection, setSelection] = useState(() => + normalizeSelectionMap(loadSelection()) + ); + const [lastSearchQuery, setLastSearchQuery] = useState(''); + const [resultQualitySelection, setResultQualitySelection] = useState({}); + const [qualitySizeCache, setQualitySizeCache] = useState({}); + const [output, setOutput] = useState(''); + + const [loadingChannels, setLoadingChannels] = useState(false); + const [loadingSearch, setLoadingSearch] = useState(false); + const [loadingPyloadSettings, setLoadingPyloadSettings] = useState(false); + const [savingPyloadSettings, setSavingPyloadSettings] = useState(false); + const [sendingToPyload, setSendingToPyload] = useState(false); + const [resultFirst, setResultFirst] = useState(0); + + const [singlePackageDialogVisible, setSinglePackageDialogVisible] = useState(false); + const [singlePackageName, setSinglePackageName] = useState(''); + const [settingsDialogVisible, setSettingsDialogVisible] = useState(false); + const [mobilePanel, setMobilePanel] = useState(''); + const [isMobileViewport, setIsMobileViewport] = useState( + () => (typeof window !== 'undefined' ? window.innerWidth <= 768 : false) + ); + + const [pyloadSettings, setPyloadSettings] = useState({ + server: '', + username: '', + password: '', + resultsPerPage: DEFAULT_ROWS + }); + const [settingsLoaded, setSettingsLoaded] = useState(false); + + const channelOptions = useMemo( + () => channels.map((channel) => ({ label: channel, value: channel })), + [channels] + ); + + const qualityOptionsByResult = useMemo(() => { + const map = {}; + results.forEach((item) => { + map[item.id] = (item.qualities || []).map((q) => ({ + label: q.label, + value: q.id + })); + }); + return map; + }, [results]); + + const selectionCount = useMemo(() => Object.keys(selection).length, [selection]); + + const selectedItems = useMemo( + () => + Object.entries(selection) + .map(([id, entry]) => { + const qualityOptions = (entry.qualities || []).map((q) => ({ + label: q.label, + value: q.id + })); + const qualityLabel = qualityOptions.find((q) => q.value === entry.selectedQuality)?.label || '-'; + + return { + id, + video: Boolean(entry.video), + sub: Boolean(entry.sub), + title: entry.title || 'Unbekannte Sendung', + channel: entry.channel || 'Unbekannter Sender', + timestamp: Number(entry.timestamp) || 0, + subtitleUrl: entry.subtitleUrl || '', + qualityOptions, + selectedQuality: entry.selectedQuality || '', + qualityLabel + }; + }) + .sort((a, b) => b.timestamp - a.timestamp), + [selection] + ); + + useEffect(() => { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(selection)); + }, [selection]); + + useEffect(() => { + async function fetchChannels() { + setLoadingChannels(true); + try { + const response = await fetch('/channels'); + if (!response.ok) { + throw new Error(`Sender konnten nicht geladen werden (${response.status})`); + } + const list = await response.json(); + setChannels(Array.isArray(list) ? list : []); + } catch (error) { + toast.current?.show({ + severity: 'error', + summary: 'Fehler', + detail: error.message, + life: 4000 + }); + } finally { + setLoadingChannels(false); + } + } + + fetchChannels(); + }, []); + + useEffect(() => { + function handleResize() { + setIsMobileViewport(window.innerWidth <= 768); + } + + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + if (!isMobileViewport) { + setMobilePanel(''); + } else { + setSettingsDialogVisible(false); + } + }, [isMobileViewport]); + + useEffect(() => { + async function fetchPyloadSettings() { + setLoadingPyloadSettings(true); + try { + const response = await fetch('/pyload/settings'); + if (!response.ok) { + throw new Error(`Einstellungen konnten nicht geladen werden (${response.status})`); + } + + const data = await response.json(); + const rows = Number(data.results_per_page) || DEFAULT_ROWS; + setPyloadSettings({ + server: data.server || '', + username: data.username || '', + password: data.password || '', + resultsPerPage: ROW_OPTIONS.includes(rows) ? rows : DEFAULT_ROWS + }); + setSettingsLoaded(true); + } catch (error) { + toast.current?.show({ + severity: 'error', + summary: 'Fehler', + detail: error.message, + life: 4000 + }); + } finally { + setLoadingPyloadSettings(false); + } + } + + fetchPyloadSettings(); + }, []); + + async function executeSearch(searchQuery, channelFilter, signal) { + const trimmed = (searchQuery || '').trim(); + if (trimmed.length < 2) { + setResults([]); + setResultFirst(0); + setLoadingSearch(false); + setLastSearchQuery(''); + return; + } + + setLastSearchQuery(trimmed); + setLoadingSearch(true); + try { + const params = new URLSearchParams(); + params.set('q', trimmed); + channelFilter.forEach((channel) => params.append('channels[]', channel)); + + const response = await fetch(`/search?${params.toString()}`, { signal }); + if (!response.ok) { + throw new Error(`Suche fehlgeschlagen (${response.status})`); + } + + const data = await response.json(); + setResults(Array.isArray(data) ? data : []); + setResultFirst(0); + setOutput(''); + } catch (error) { + if (error.name !== 'AbortError') { + toast.current?.show({ + severity: 'error', + summary: 'Fehler', + detail: error.message, + life: 4000 + }); + } + } finally { + if (!signal || !signal.aborted) { + setLoadingSearch(false); + } + } + } + + function handleChannelFilterChange(value) { + const nextChannels = Array.isArray(value) ? value : []; + setSelectedChannels(nextChannels); + + // Auto-filter only when a result list is already visible. + if (results.length > 0 && lastSearchQuery.length >= 2) { + executeSearch(lastSearchQuery, nextChannels); + } + } + + function openSettingsPanel() { + if (isMobileViewport) { + setMobilePanel((previous) => (previous === 'settings' ? '' : 'settings')); + return; + } + setSettingsDialogVisible(true); + } + + async function persistResultsPerPage(rows) { + try { + const response = await fetch('/pyload/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + results_per_page: rows + }) + }); + + if (!response.ok) { + throw new Error(`Einstellungen konnten nicht gespeichert werden (${response.status})`); + } + } catch (error) { + toast.current?.show({ + severity: 'error', + summary: 'Fehler', + detail: error.message, + life: 3500 + }); + } + } + + function handleResultPageChange(event) { + const nextFirst = Number(event.first) || 0; + const nextRows = Number(event.rows) || DEFAULT_ROWS; + const currentRows = Number(pyloadSettings.resultsPerPage) || DEFAULT_ROWS; + + setResultFirst(nextFirst); + + if (nextRows !== currentRows) { + setResultFirst(0); + setPyloadSettings((previous) => ({ + ...previous, + resultsPerPage: nextRows + })); + + if (settingsLoaded) { + persistResultsPerPage(nextRows); + } + } + } + + function getResultSelectedQuality(item) { + const qualityFromSelection = selection[item.id]?.selectedQuality; + if (qualityFromSelection) return qualityFromSelection; + + const qualityFromDraft = resultQualitySelection[item.id]; + if (qualityFromDraft) return qualityFromDraft; + + return item.default_quality || item.qualities?.[0]?.id || ''; + } + + async function resolveQualitySizesForItem(item) { + const qualities = Array.isArray(item.qualities) ? item.qualities : []; + const urls = qualities + .map((quality) => { + if (!quality?.url) return ''; + const directSize = normalizePositiveSize(quality.size); + if (directSize > 0) return ''; + if (normalizePositiveSize(qualitySizeCache[quality.url]) > 0) return ''; + return quality.url; + }) + .filter(Boolean); + + if (!urls.length) return; + + try { + const response = await fetch('/quality-sizes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ urls }) + }); + + if (!response.ok) { + return; + } + + const payload = await response.json(); + const sizes = payload?.sizes; + if (!sizes || typeof sizes !== 'object') { + return; + } + + setQualitySizeCache((previous) => ({ + ...previous, + ...sizes + })); + } catch { + // Size lookup is optional; keep the UI responsive if a host blocks HEAD/Range. + } + } + + function updateSelectionEntry(id, updater) { + setSelection((previous) => { + const current = previous[id]; + if (!current) return previous; + + const next = { ...previous }; + const updated = updater({ ...current }); + + if (!updated.video && !updated.sub) { + delete next[id]; + } else { + next[id] = { + ...updated, + videoUrl: getSelectedVideoUrl(updated) + }; + } + return next; + }); + } + + function toggleSelectionFromResult(item, type, checked) { + const draftQuality = resultQualitySelection[item.id] || ''; + + setSelection((previous) => { + const next = { ...previous }; + const existing = next[item.id] || {}; + const entry = buildSelectionEntryFromResult(item, existing, draftQuality); + entry[type] = checked; + + if (!entry.video && !entry.sub) { + delete next[item.id]; + } else { + next[item.id] = entry; + } + return next; + }); + + if (checked) { + resolveQualitySizesForItem(item); + } + } + + function handleResultQualityChange(item, qualityId) { + const selectedQuality = pickSelectedQuality(item.qualities || [], qualityId); + + setResultQualitySelection((previous) => ({ + ...previous, + [item.id]: selectedQuality + })); + + setSelection((previous) => { + if (!previous[item.id]) { + return previous; + } + + const next = { ...previous }; + const existing = next[item.id]; + const entry = buildSelectionEntryFromResult(item, existing, selectedQuality); + entry.selectedQuality = pickSelectedQuality(entry.qualities, selectedQuality); + + if (!entry.video && !entry.sub) { + delete next[item.id]; + } else { + entry.videoUrl = getSelectedVideoUrl(entry); + next[item.id] = entry; + } + return next; + }); + + resolveQualitySizesForItem(item); + } + + function handleSelectedQualityChange(itemId, qualityId) { + setResultQualitySelection((previous) => ({ + ...previous, + [itemId]: qualityId + })); + + updateSelectionEntry(itemId, (entry) => ({ + ...entry, + selectedQuality: pickSelectedQuality(entry.qualities || [], qualityId) + })); + + const item = results.find((result) => result.id === itemId); + if (item) { + resolveQualitySizesForItem(item); + } + } + + function removeSelection(id) { + setSelection((previous) => { + if (!previous[id]) return previous; + const next = { ...previous }; + delete next[id]; + return next; + }); + } + + function buildSelectedLinks() { + const links = []; + + Object.values(selection).forEach((entry) => { + if (entry.video) { + const videoUrl = getSelectedVideoUrl(entry); + if (videoUrl) links.push(videoUrl); + } + + if (entry.sub && entry.subtitleUrl) { + links.push(entry.subtitleUrl); + } + }); + + return links; + } + + function handleExport() { + const links = buildSelectedLinks(); + setOutput(links.join('\n')); + + if (!links.length) { + toast.current?.show({ + severity: 'warn', + summary: 'Keine Links', + detail: 'Bitte zuerst Treffer auswählen.', + life: 3000 + }); + } + } + + function buildIndividualPackages() { + return Object.values(selection) + .map((entry) => { + const links = []; + + if (entry.video) { + const videoUrl = getSelectedVideoUrl(entry); + if (videoUrl) links.push(videoUrl); + } + + if (entry.sub && entry.subtitleUrl) { + links.push(entry.subtitleUrl); + } + + return { + name: entry.title || 'Unbenannt', + links + }; + }) + .filter((entry) => entry.links.length > 0); + } + + async function savePyloadSettings(showSuccessToast = true) { + setSavingPyloadSettings(true); + try { + const response = await fetch('/pyload/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + server: pyloadSettings.server, + username: pyloadSettings.username, + password: pyloadSettings.password, + results_per_page: pyloadSettings.resultsPerPage + }) + }); + + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error || `Speichern fehlgeschlagen (${response.status})`); + } + + if (showSuccessToast) { + toast.current?.show({ + severity: 'success', + summary: 'Settings', + detail: 'Einstellungen gespeichert.', + life: 3000 + }); + } + + return true; + } catch (error) { + toast.current?.show({ + severity: 'error', + summary: 'Fehler', + detail: error.message, + life: 4500 + }); + return false; + } finally { + setSavingPyloadSettings(false); + } + } + + async function sendPackagesToPyload(packages) { + if (!pyloadSettings.server || !pyloadSettings.username || !pyloadSettings.password) { + toast.current?.show({ + severity: 'warn', + summary: 'Fehlende Daten', + detail: 'Bitte pyLoad-Server, Benutzer und Passwort eintragen.', + life: 3500 + }); + return false; + } + + const settingsSaved = await savePyloadSettings(false); + if (!settingsSaved) { + return false; + } + + if (!packages.length) { + toast.current?.show({ + severity: 'warn', + summary: 'Keine Pakete', + detail: 'In der Auswahl sind keine gültigen Download-Links.', + life: 3500 + }); + return false; + } + + setSendingToPyload(true); + try { + const response = await fetch('/pyload/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + server: pyloadSettings.server, + username: pyloadSettings.username, + password: pyloadSettings.password, + packages + }) + }); + + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error || `pyLoad-Fehler (${response.status})`); + } + + const addedCount = payload.added_count || 0; + const failedCount = payload.failed_count || 0; + + toast.current?.show({ + severity: failedCount > 0 ? 'warn' : 'success', + summary: 'pyLoad', + detail: failedCount > 0 + ? `${addedCount} hinzugefügt, ${failedCount} fehlgeschlagen.` + : `${addedCount} Pakete hinzugefügt.`, + life: 4500 + }); + return true; + } catch (error) { + toast.current?.show({ + severity: 'error', + summary: 'pyLoad', + detail: error.message, + life: 4500 + }); + return false; + } finally { + setSendingToPyload(false); + } + } + + async function handleSendAsIndividualPackages() { + const packages = buildIndividualPackages(); + await sendPackagesToPyload(packages); + } + + function openSinglePackageDialog() { + setSinglePackageName(''); + setSinglePackageDialogVisible(true); + } + + async function handleSendAsSinglePackage() { + const packageName = singlePackageName.trim(); + if (!packageName) { + toast.current?.show({ + severity: 'warn', + summary: 'Package-Name fehlt', + detail: 'Bitte einen Package-Namen eingeben.', + life: 3000 + }); + return; + } + + const links = buildSelectedLinks(); + const success = await sendPackagesToPyload([{ name: packageName, links }]); + if (success) { + setSinglePackageDialogVisible(false); + setSinglePackageName(''); + } + } + + function resultTemplate(item) { + const selected = selection[item.id] || {}; + const selectedQuality = getResultSelectedQuality(item); + const selectedSize = getQualitySizeForItem(item, selectedQuality, qualitySizeCache); + const qualityOptions = qualityOptionsByResult[item.id] || []; + const videoId = `video-${item.id}`; + const subtitleId = `subtitle-${item.id}`; + + return ( +
+
+
+
{item.title}
+
+ + + + +
+
+ +
+
+ toggleSelectionFromResult(item, 'video', event.checked)} + /> +
+ +
+ handleResultQualityChange(item, event.value)} + className="quality-compact quality-compact-main" + placeholder="Q" + disabled={!qualityOptions.length} + /> +
+ +
+
+ {item.subtitle ? ( + toggleSelectionFromResult(item, 'sub', event.checked)} + /> + ) : ( + - + )} +
+
+ +
+
+
+
+
+ ); + } + + function selectedTemplate(item) { + return ( +
+
+
{item.title}
+
{item.channel} • {formatDate(item.timestamp)}
+
+ +
+
+ +
+ + updateSelectionEntry(item.id, (entry) => ({ ...entry, video: event.checked })) + } + /> +
+
+ +
+ + handleSelectedQualityChange(item.id, event.value)} + className="quality-compact quality-compact-main mt-2" + disabled={!item.qualityOptions.length} + /> +
+ +
+ +
+ {item.subtitleUrl ? ( + + updateSelectionEntry(item.id, (entry) => ({ ...entry, sub: event.checked })) + } + /> + ) : ( + - + )} +
+
+ +
+
+
+
+ ); + } + + function renderSearchCard(extraClass = '') { + return ( + +
+
+ + setQuery(event.target.value)} + className="w-full" + placeholder="z.B. Arte, Tatort, Doku ..." + /> +
+ +
+ + handleChannelFilterChange(event.value)} + optionLabel="label" + optionValue="value" + placeholder="Alle Sender" + display="chip" + filter + loading={loadingChannels} + className="w-full" + maxSelectedLabels={2} + /> +
+ +
+
+
+
+ ); + } + + function renderSelectionCard(extraClass = '') { + return ( + + {selectionCount > 0 ? ( +
+
+
+
+
+
+ ) : null} + + +
+ + ); + } + + return ( +
+ + + { + if (!sendingToPyload) setSinglePackageDialogVisible(false); + }} + footer={ +
+
+ } + > + + setSinglePackageName(event.target.value)} + className="w-full" + autoFocus + disabled={sendingToPyload} + /> +
+ + {!isMobileViewport ? ( + setSettingsDialogVisible(false)} + > + {renderSettingsContent()} + + ) : null} + +
+
+
+
Mediathek Control Center
+
Treffer verwalten und direkt an pyLoad senden
+
+
+ + + {!isMobileViewport ? ( +
+
+
+ +
+
+ {!isMobileViewport ? renderSearchCard() : null} + + {!isMobileViewport ? ( + + ) : null} + + +
+
Titel
+
Video
+
Qualität
+
UT
+
Aktion
+
+ +
+
+
+ +
+
+
+
+ + {isMobileViewport && mobilePanel ? ( +
setMobilePanel('')} + > +
event.stopPropagation()} + > +
+
+ {mobilePanel === 'search' + ? 'Suche' + : mobilePanel === 'selection' + ? `Vorgemerkt (${selectionCount})` + : 'Einstellungen'} +
+
+
+ {mobilePanel === 'search' ? renderSearchCard('mobile-panel-card') : null} + {mobilePanel === 'selection' ? renderSelectionCard('mobile-panel-card') : null} + {mobilePanel === 'settings' ? ( + + {renderSettingsContent()} + + ) : null} +
+
+
+ ) : null} +
+ ); +} + +export default App; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..e57e476 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { PrimeReactProvider } from 'primereact/api'; +import 'primereact/resources/themes/lara-dark-teal/theme.css'; +import 'primereact/resources/primereact.min.css'; +import 'primeicons/primeicons.css'; +import 'primeflex/primeflex.css'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..b4013e8 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,502 @@ +html, +body, +#root { + margin: 0; + padding: 0; +} + +.app-shell { + min-height: 100vh; + padding-bottom: 5rem; + background: + radial-gradient(1200px 420px at 25% -8%, color-mix(in srgb, var(--primary-color) 25%, transparent), transparent 60%), + linear-gradient(180deg, #08132a 0%, #071125 45%, #071022 100%); +} + +.app-topbar { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid var(--surface-border); + background: color-mix(in srgb, var(--surface-ground) 90%, black 10%); + backdrop-filter: blur(8px); +} + +.app-topbar-inner { + max-width: 1120px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.8rem 1rem; +} + +.app-brand { + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.app-brand-title { + font-size: 0.88rem; + font-weight: 600; + line-height: 1.15; +} + +.app-brand-subtitle { + font-size: 0.62rem; + color: var(--text-color-secondary); +} + +.app-main { + max-width: 1120px; + margin: 1rem auto; + padding: 0 1rem 1.5rem; +} + +.app-content-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-areas: + "search" + "selection" + "results"; + gap: 1rem; + align-items: start; +} + +.search-card { + grid-area: search; +} + +.selection-panel { + grid-area: selection; +} + +.results-card { + grid-area: results; +} + +@media (min-width: 1260px) { + .app-shell { + height: 100vh; + min-height: 100vh; + padding-bottom: 0; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .app-main { + width: 100%; + flex: 1 1 auto; + height: calc(100vh - 3.4rem); + min-height: 0; + margin: 0 auto; + padding: 1rem; + overflow: hidden; + } + + .app-content-grid { + height: 100%; + grid-template-columns: minmax(0, 1fr) 23.5rem; + grid-template-rows: auto minmax(0, 1fr); + grid-template-areas: + "results search" + "results selection"; + } + + .results-card { + height: 100%; + max-height: calc(100dvh - 8.5rem); + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .results-card .p-card-body { + height: 100%; + display: flex; + flex-direction: column; + min-height: 0; + } + + .results-card .p-card-content { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + } + + .results-card .p-dataview { + height: 100%; + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + } + + .results-card .p-dataview-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto !important; + overflow-x: hidden; + } + + .results-card .p-paginator { + flex: 0 0 auto; + } +} + +.app-bottom-nav { + display: none; + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 30; + padding: 0.6rem 0.85rem max(0.6rem, env(safe-area-inset-bottom)); + border-top: 1px solid var(--surface-border); + background: color-mix(in srgb, var(--surface-ground) 92%, black 8%); +} + +.app-bottom-nav-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.45rem; +} + +.app-bottom-nav .p-button { + background: transparent; + border: none; +} + +.app-bottom-nav .p-button.active { + background: color-mix(in srgb, var(--primary-color) 20%, transparent); + color: var(--primary-color-text); +} + +.mobile-sheet-backdrop { + position: fixed; + inset: 0; + z-index: 45; + background: rgba(5, 10, 18, 0.68); + display: grid; + align-items: end; +} + +.mobile-sheet-card { + max-height: 82vh; + overflow-y: auto; + border-radius: 1rem 1rem 0 0; + border: 1px solid var(--surface-border); + background: color-mix(in srgb, var(--surface-ground) 92%, black 8%); + padding: 0.75rem; +} + +.mobile-sheet-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.6rem; +} + +.mobile-sheet-content { + display: grid; + gap: 0.7rem; +} + +.mobile-panel-card { + margin: 0; +} + +.result-row { + width: 100%; + margin: 0; + border-radius: 0; + border-bottom: 1px solid var(--surface-border); + background: color-mix(in srgb, var(--surface-card) 92%, black 8%); +} + +.result-label { + display: inline-block; + font-size: 0.64rem; + font-weight: 600; + color: var(--text-color-secondary); + letter-spacing: 0.02em; +} + +.result-grid { + margin: 0; +} + +.result-table-head, +.result-table-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 4.2rem 6.4rem 4.6rem 2.8rem; + column-gap: 0.6rem; + align-items: center; +} + +.result-table-row { + padding: 0.5rem 0.75rem; +} + +.result-controls-group { + display: contents; +} + +.result-title { + font-size: 0.74rem; + line-height: 1.2; + margin-bottom: 0.35rem; +} + +.result-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.result-muted { + color: var(--text-color-secondary); +} + +.result-columns { + margin: 0 0 0.35rem; + padding: 0.45rem 0.75rem; + background: color-mix(in srgb, var(--surface-card) 78%, black 22%); + border: 1px solid var(--surface-border); + border-radius: 0.45rem; + font-size: 0.6rem; + font-weight: 600; + color: var(--text-color-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.selection-row { + width: 100%; + margin: 0; + border-radius: 0; + border-bottom: 1px solid var(--surface-border); + background: color-mix(in srgb, var(--surface-card) 95%, black 5%); +} + +.selection-header { + padding: 0.6rem 0.75rem 0.35rem; +} + +.selection-meta { + margin-top: 0.1rem; + font-size: 0.68rem; +} + +.selection-controls { + margin: 0; + padding: 0.2rem 0.75rem 0.6rem; + border-top: 1px solid color-mix(in srgb, var(--surface-border) 70%, transparent); +} + +.quality-compact { + width: 5.4rem; + min-width: 5.4rem; +} + +.quality-compact .p-dropdown-label { + padding-right: 1.8rem; +} + +.quality-compact-main .p-dropdown-label { + text-align: left; +} + +.panel-card { + border: 1px solid var(--surface-border); + box-shadow: 0 10px 28px color-mix(in srgb, black 22%, transparent); +} + +.panel-card .p-card-title { + margin-bottom: 0.5rem; + font-size: 0.76rem; + letter-spacing: 0.02em; +} + +.panel-card .p-card-body { + padding: 0.85rem 1rem 1rem; +} + +.panel-card .p-card-content { + padding: 0; +} + +.search-panel .p-card-body { + padding-bottom: 0.75rem; +} + +.app-shell .p-inputtext, +.app-shell .p-dropdown, +.app-shell .p-multiselect, +.app-shell .p-button, +.app-shell .p-tag, +.app-shell .p-checkbox-label { + font-size: 0.7rem; +} + +.app-shell .p-dropdown-label, +.app-shell .p-multiselect-label { + font-size: 0.7rem; +} + +.app-shell, +.app-shell .text-sm, +.app-shell .text-xs, +.app-shell .text-600 { + font-size: 0.72rem; +} + +.app-shell .p-component, +.app-shell .p-tag .p-tag-value, +.app-shell .p-dropdown-panel .p-dropdown-items .p-dropdown-item, +.app-shell .p-multiselect-panel .p-multiselect-items .p-multiselect-item, +.app-shell .p-multiselect-token, +.app-shell .p-button .p-button-label, +.app-shell .p-inputtext, +.app-shell .p-checkbox + label, +.app-shell .p-checkbox-label, +.app-shell .p-dropdown .p-dropdown-label, +.app-shell .p-multiselect .p-multiselect-label { + font-size: 0.68rem !important; +} + +.app-shell .p-tag { + padding: 0.2rem 0.4rem; +} + +.app-shell .p-inputtext, +.app-shell .p-dropdown .p-dropdown-label, +.app-shell .p-multiselect .p-multiselect-label { + padding-top: 0.42rem; + padding-bottom: 0.42rem; +} + +.app-shell .p-dataview-content > .grid { + margin: 0; +} + +.app-shell .p-dataview-content > .grid > .col-12 { + padding: 0; +} + +.app-shell .p-paginator { + padding: 0.65rem 0 0; + border: 0; + background: transparent; +} + +.app-shell .p-paginator .p-dropdown { + min-width: 5rem; + height: 2.2rem; +} + +.app-shell .p-paginator .p-dropdown .p-dropdown-label { + display: flex; + align-items: center; + height: 100%; + padding-top: 0; + padding-bottom: 0; + line-height: 1; +} + +.app-shell .p-paginator .p-dropdown .p-dropdown-trigger { + width: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +@media (max-width: 768px) { + .app-bottom-nav { + display: block; + } + + .app-brand-subtitle { + display: none; + } + + .app-main { + padding: 0 0.75rem 1.5rem; + } + + .app-content-grid { + grid-template-areas: "results"; + } + + .panel-card .p-card-body { + padding: 0.75rem; + } + + .mobile-sheet-card .panel-card .p-card-body { + padding: 0.7rem; + } + + .result-table-head { + display: none; + } + + .result-table-row { + grid-template-columns: minmax(0, 40%) minmax(0, 60%); + grid-template-areas: "title controls"; + row-gap: 0; + column-gap: 0.7rem; + align-items: center; + padding: 0.55rem 0.6rem; + } + + .result-col-title { + grid-area: title; + width: 100%; + max-width: 100%; + min-width: 0; + } + + .result-controls-group { + grid-area: controls; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + align-items: center; + justify-content: flex-end; + justify-items: center; + width: 100%; + } + + .result-col-video, + .result-col-ut, + .result-col-action { + width: 100%; + min-width: 0; + justify-content: center !important; + } + + .result-col-quality { + width: 100%; + justify-content: center !important; + } + + .result-tags { + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.32rem; + justify-content: stretch; + align-content: start; + width: 100%; + } + + .quality-compact { + width: 100%; + min-width: 0; + max-width: 4.9rem; + } +} diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..58b349d --- /dev/null +++ b/start.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT_DIR" + +SKIP_BUILD=0 +if [[ "${1:-}" == "--skip-build" ]]; then + SKIP_BUILD=1 +fi + +if [[ ! -d node_modules ]]; then + echo "Installing frontend dependencies..." + npm install +fi + +if [[ $SKIP_BUILD -eq 0 ]]; then + echo "Building frontend..." + npm run build +fi + +echo "Starting Flask app on http://127.0.0.1:5000 ..." +exec python3 app.py diff --git a/vite.config.mjs b/vite.config.mjs new file mode 100644 index 0000000..a792b1e --- /dev/null +++ b/vite.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + proxy: { + '/channels': 'http://127.0.0.1:5000', + '/search': 'http://127.0.0.1:5000' + } + } +});