forked from tpd94/CDRM-Project
UI overhaul
This commit is contained in:
parent
cc3b37db1d
commit
f83d22c09e
@ -4,5 +4,6 @@
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"useTabs": false,
|
||||
"printWidth": 100
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
|
137
cdrm-frontend/package-lock.json
generated
137
cdrm-frontend/package-lock.json
generated
@ -12,8 +12,10 @@
|
||||
"axios": "^1.10.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"shaka-player": "^4.15.8",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -22,10 +24,12 @@
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||
"daisyui": "^5.0.46",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"vite": "^7.0.5"
|
||||
}
|
||||
},
|
||||
@ -2137,6 +2141,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.0.46",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.46.tgz",
|
||||
"integrity": "sha512-vMDZK1tI/bOb2Mc3Mk5WpquBG3ZqBz1YKZ0xDlvpOvey60dOS4/5Qhdowq1HndbQl7PgDLDYysxAjjUjwR7/eQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@ -3479,6 +3493,110 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-hermes": "*",
|
||||
"@prettier/plugin-oxc": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"@zackad/prettier-plugin-twig": "*",
|
||||
"prettier": "^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-import-sort": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-multiline-arrays": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
"prettier-plugin-style-order": "*",
|
||||
"prettier-plugin-svelte": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-hermes": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-oxc": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@zackad/prettier-plugin-twig": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-import-sort": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-multiline-arrays": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-style-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@ -3516,6 +3634,15 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@ -3667,6 +3794,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
|
||||
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
@ -14,8 +14,10 @@
|
||||
"axios": "^1.10.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"shaka-player": "^4.15.8",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -24,10 +26,12 @@
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||
"daisyui": "^5.0.46",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"vite": "^7.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import Home from "./components/Pages/HomePage";
|
||||
import Cache from "./components/Pages/Cache";
|
||||
import API from "./components/Pages/API";
|
||||
import TestPlayer from "./components/Pages/TestPlayer";
|
||||
import NavBar from "./components/NavBar";
|
||||
import NavBarMain from "./components/NavBarMain";
|
||||
import SideMenu from "./components/SideMenu"; // Add this import
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import Account from "./components/Pages/Account";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import API from "./components/Pages/API";
|
||||
import Cache from "./components/Pages/Cache";
|
||||
import Home from "./components/Pages/HomePage";
|
||||
import TestPlayer from "./components/Pages/TestPlayer";
|
||||
|
||||
function App() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
|
||||
|
||||
return (
|
||||
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
|
||||
{/* The SideMenu should be visible when isMenuOpen is true */}
|
||||
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
|
||||
|
||||
<div
|
||||
id="navbarcontainer"
|
||||
className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0"
|
||||
>
|
||||
<NavBar />
|
||||
</div>
|
||||
|
||||
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
|
||||
<div
|
||||
id="navbarmaincontainer"
|
||||
className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10"
|
||||
>
|
||||
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
|
||||
</div>
|
||||
|
||||
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/cache" element={<Cache />} />
|
||||
<Route path="/api" element={<API />} />
|
||||
<Route path="/testplayer" element={<TestPlayer />} />
|
||||
<Route path="/account" element={<Account />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/cache" element={<Cache />} />
|
||||
<Route path="/api" element={<API />} />
|
||||
<Route path="/testplayer" element={<TestPlayer />} />
|
||||
<Route path="/account" element={<Account />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
|
BIN
cdrm-frontend/src/assets/fonts/InterVariable-Italic.woff2
Normal file
BIN
cdrm-frontend/src/assets/fonts/InterVariable-Italic.woff2
Normal file
Binary file not shown.
BIN
cdrm-frontend/src/assets/fonts/InterVariable.woff2
Normal file
BIN
cdrm-frontend/src/assets/fonts/InterVariable.woff2
Normal file
Binary file not shown.
15
cdrm-frontend/src/assets/fonts/font-face.css
Normal file
15
cdrm-frontend/src/assets/fonts/font-face.css
Normal file
@ -0,0 +1,15 @@
|
||||
@font-face {
|
||||
font-family: Inter;
|
||||
src: url("./InterVariable.woff2");
|
||||
font-style: normal;
|
||||
font-weight: 300 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Inter;
|
||||
src: url("./InterVariable-Italic.woff2");
|
||||
font-style: italic;
|
||||
font-weight: 300 900;
|
||||
font-display: swap;
|
||||
}
|
9
cdrm-frontend/src/components/Container.jsx
Normal file
9
cdrm-frontend/src/components/Container.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
const Container = ({ children, className = "", ...props }) => {
|
||||
return (
|
||||
<main className={`container mx-auto p-4 mb-5 ${className}`} {...props}>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
@ -1,13 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import homeIcon from "../assets/icons/home.svg";
|
||||
import cacheIcon from "../assets/icons/cache.svg";
|
||||
import apiIcon from "../assets/icons/api.svg";
|
||||
import testPlayerIcon from "../assets/icons/testplayer.svg";
|
||||
import accountIcon from "../assets/icons/account.svg";
|
||||
import discordIcon from "../assets/icons/discord.svg";
|
||||
import telegramIcon from "../assets/icons/telegram.svg";
|
||||
import giteaIcon from "../assets/icons/gitea.svg";
|
||||
import { FaDiscord } from "react-icons/fa";
|
||||
import { FaTelegram } from "react-icons/fa";
|
||||
import { SiGitea } from "react-icons/si";
|
||||
import { FaHome } from "react-icons/fa";
|
||||
import { FaDatabase } from "react-icons/fa";
|
||||
import { IoCodeSlashSharp } from "react-icons/io5";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
import { RiAccountCircleFill } from "react-icons/ri";
|
||||
|
||||
function NavBar() {
|
||||
const [externalLinks, setExternalLinks] = useState({
|
||||
@ -23,152 +23,156 @@ function NavBar() {
|
||||
.catch((error) => console.error("Error fetching links:", error));
|
||||
}, []);
|
||||
|
||||
const MenuItem = ({ to, children }) => {
|
||||
return (
|
||||
<li>
|
||||
<NavLink to={to} className={({ isActive }) => (isActive ? "menu-active" : "")}>
|
||||
{children}
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full bg-white/1">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<p className="text-white text-2xl font-bold p-3 text-center mb-5">
|
||||
<a href="/">CDRM-Project</a>
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<div className="navbar sticky top-0 z-300 bg-slate-700 shadow-sm text-white">
|
||||
<div className="navbar-start">
|
||||
<div className="dropdown">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
{" "}
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
/>{" "}
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
<MenuItem to="/">
|
||||
<FaHome alt="Home" width={20} height={20} />
|
||||
Home
|
||||
</MenuItem>
|
||||
<MenuItem to="/cache">
|
||||
<FaDatabase alt="Cache" width={20} height={20} />
|
||||
Cache
|
||||
</MenuItem>
|
||||
<MenuItem to="/api">
|
||||
<IoCodeSlashSharp alt="API" width={20} height={20} />
|
||||
API
|
||||
</MenuItem>
|
||||
<MenuItem to="/testplayer">
|
||||
<FaVideo alt="Test Player" width={20} height={20} />
|
||||
Test Player
|
||||
</MenuItem>
|
||||
<MenuItem to="/account">
|
||||
<RiAccountCircleFill alt="My Account" width={20} height={20} />
|
||||
My Account
|
||||
</MenuItem>
|
||||
|
||||
{/* Scrollable navigation area */}
|
||||
<div className="overflow-y-auto grow flex flex-col">
|
||||
{/* Main NavLinks */}
|
||||
<NavLink
|
||||
to="/"
|
||||
className={({ isActive }) =>
|
||||
`flex flex-row p-3 border-l-3 ${
|
||||
isActive
|
||||
? "border-l-sky-500/50 bg-black/50"
|
||||
: "hover:border-l-sky-500/50 hover:bg-white/5"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
||||
<img src={homeIcon} alt="Home" className="w-1/2 cursor-pointer" />
|
||||
</button>
|
||||
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
||||
Home
|
||||
</p>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/cache"
|
||||
className={({ isActive }) =>
|
||||
`flex flex-row p-3 border-l-3 ${
|
||||
isActive
|
||||
? "border-l-emerald-500/50 bg-black/50"
|
||||
: "hover:border-l-emerald-500/50 hover:bg-white/5"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
||||
<img src={cacheIcon} alt="Cache" className="w-1/2 cursor-pointer" />
|
||||
</button>
|
||||
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
||||
Cache
|
||||
</p>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/api"
|
||||
className={({ isActive }) =>
|
||||
`flex flex-row p-3 border-l-3 ${
|
||||
isActive
|
||||
? "border-l-indigo-500/50 bg-black/50"
|
||||
: "hover:border-l-indigo-500/50 hover:bg-white/5"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
||||
<img src={apiIcon} alt="API" className="w-1/2 cursor-pointer" />
|
||||
</button>
|
||||
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
||||
API
|
||||
</p>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/testplayer"
|
||||
className={({ isActive }) =>
|
||||
`flex flex-row p-3 border-l-3 ${
|
||||
isActive
|
||||
? "border-l-rose-500/50 bg-black/50"
|
||||
: "hover:border-l-rose-500/50 hover:bg-white/5"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
||||
<img
|
||||
src={testPlayerIcon}
|
||||
alt="Test Player"
|
||||
className="w-1/2 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
||||
Test Player
|
||||
</p>
|
||||
</NavLink>
|
||||
|
||||
{/* Account link at bottom of scrollable area */}
|
||||
<div className="mt-auto">
|
||||
<NavLink
|
||||
to="/account"
|
||||
className={({ isActive }) =>
|
||||
`flex flex-row p-3 border-l-3 ${
|
||||
isActive
|
||||
? "border-l-yellow-500/50 bg-black/50"
|
||||
: "hover:border-l-yellow-500/50 hover:bg-white/5"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
||||
<img src={accountIcon} alt="Account" className="w-1/2 cursor-pointer" />
|
||||
</button>
|
||||
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
||||
<div className="divider">Social links</div>
|
||||
<li>
|
||||
<a
|
||||
href={externalLinks.discord}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaDiscord alt="Discord" width={20} height={20} />
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={externalLinks.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FaTelegram alt="Telegram" width={20} height={20} />
|
||||
Telegram
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={externalLinks.gitea}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<SiGitea alt="Gitea" width={20} height={20} />
|
||||
Gitea
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a className="btn btn-ghost text-xl">CDRM-Project</a>
|
||||
</div>
|
||||
<div className="navbar-center hidden lg:flex">
|
||||
<ul className="menu menu-horizontal px-1">
|
||||
<MenuItem to="/">
|
||||
<FaHome alt="Home" width={20} height={20} />
|
||||
Home
|
||||
</MenuItem>
|
||||
<MenuItem to="/cache">
|
||||
<FaDatabase alt="Cache" width={20} height={20} />
|
||||
Cache
|
||||
</MenuItem>
|
||||
<MenuItem to="/api">
|
||||
<IoCodeSlashSharp alt="API" width={20} height={20} />
|
||||
API
|
||||
</MenuItem>
|
||||
<MenuItem to="/testplayer">
|
||||
<FaVideo alt="Test Player" width={20} height={20} />
|
||||
Test Player
|
||||
</MenuItem>
|
||||
<MenuItem to="/account">
|
||||
<RiAccountCircleFill alt="My Account" width={20} height={20} />
|
||||
My Account
|
||||
</p>
|
||||
</NavLink>
|
||||
</MenuItem>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="navbar-end hidden lg:flex">
|
||||
<a
|
||||
className="btn btn-ghost hover:text-indigo-400"
|
||||
href={externalLinks.discord}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="tooltip tooltip-bottom" data-tip="CDRM Discord">
|
||||
<FaDiscord className="h-6 w-6" />
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
className="btn btn-ghost hover:text-sky-400"
|
||||
href={externalLinks.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="tooltip tooltip-bottom" data-tip="CDRM Telegram">
|
||||
<FaTelegram className="h-6 w-6" />
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
className="btn btn-ghost hover:text-lime-400"
|
||||
href={externalLinks.gitea}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="tooltip tooltip-left" data-tip="CDRM Gitea">
|
||||
<SiGitea className="h-6 w-6" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* External links at very bottom */}
|
||||
<div className="flex flex-row w-full h-16 bg-black/25">
|
||||
<a
|
||||
href={externalLinks.discord}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
|
||||
>
|
||||
<img
|
||||
src={discordIcon}
|
||||
alt="Discord"
|
||||
className="w-1/2 group-hover:animate-bounce"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={externalLinks.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
|
||||
>
|
||||
<img
|
||||
src={telegramIcon}
|
||||
alt="Telegram"
|
||||
className="w-1/2 group-hover:animate-bounce"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href={externalLinks.gitea}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-green-700 group"
|
||||
>
|
||||
<img src={giteaIcon} alt="Gitea" className="w-1/2 group-hover:animate-bounce" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import NavBar from "../NavBar";
|
||||
import Container from "../Container";
|
||||
import { FaCopy } from "react-icons/fa";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const { protocol, hostname, port } = window.location;
|
||||
|
||||
@ -10,6 +14,11 @@ if (
|
||||
fullHost += `:${port}`;
|
||||
}
|
||||
|
||||
const handleCopy = (text) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success("Copied to clipboard");
|
||||
};
|
||||
|
||||
function API() {
|
||||
const [deviceInfo, setDeviceInfo] = useState({
|
||||
device_type: "",
|
||||
@ -61,13 +70,7 @@ function API() {
|
||||
document.title = "API | CDRM-Project";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
|
||||
<details open className="w-full list-none">
|
||||
<summary className="text-2xl">Sending a decryption request</summary>
|
||||
<div className="mt-5 p-5 rounded-lg border-2 border-indigo-500/50">
|
||||
<pre className="rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto">
|
||||
{`import requests
|
||||
const decryptRequest = `import requests
|
||||
|
||||
print(requests.post(
|
||||
url='${fullHost}/api/decrypt',
|
||||
@ -83,55 +86,154 @@ print(requests.post(
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
})
|
||||
}
|
||||
).json()['message'])`}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
<details open className="w-full list-none mt-5">
|
||||
<summary className="text-2xl">Sending a search request</summary>
|
||||
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg">
|
||||
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
|
||||
{`import requests
|
||||
).json()['message'])`;
|
||||
|
||||
const searchRequest = `import requests
|
||||
|
||||
print(requests.post(
|
||||
url='${fullHost}/api/cache/search',
|
||||
json={
|
||||
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
|
||||
}
|
||||
).json())`}
|
||||
</pre>
|
||||
).json())`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<Container>
|
||||
<div className="mx-auto flex w-full max-w-2xl flex-col justify-center py-8">
|
||||
<div className="join join-vertical w-full max-w-2xl">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="collapse-arrow join-item collapse border border-gray-600"
|
||||
>
|
||||
<input type="checkbox" defaultChecked />
|
||||
<div className="collapse-title text-lg font-semibold">
|
||||
Sending a decryption request
|
||||
</div>
|
||||
<div className="collapse-content text-slate-300">
|
||||
<pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
|
||||
{decryptRequest}
|
||||
</pre>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => handleCopy(decryptRequest)}
|
||||
>
|
||||
<FaCopy /> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="collapse-arrow join-item collapse border border-gray-600"
|
||||
>
|
||||
<input type="checkbox" defaultChecked />
|
||||
<div className="collapse-title text-lg font-semibold">
|
||||
Sending a search request
|
||||
</div>
|
||||
<div className="collapse-content text-slate-300">
|
||||
<pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
|
||||
{searchRequest}
|
||||
</pre>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => handleCopy(searchRequest)}
|
||||
>
|
||||
<FaCopy /> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="collapse-arrow join-item collapse border border-gray-600"
|
||||
>
|
||||
<input type="checkbox" defaultChecked />
|
||||
<div className="collapse-title text-lg font-semibold">
|
||||
PyWidevine RemoteCDM info
|
||||
</div>
|
||||
<div className="collapse-content text-slate-300">
|
||||
<p>
|
||||
<strong>Device Type:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{deviceInfo.device_type || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>System ID:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{deviceInfo.system_id || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Security Level:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{deviceInfo.security_level || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Host:</strong>{" "}
|
||||
<span className="font-mono">{fullHost}/remotecdm/widevine</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Secret:</strong>{" "}
|
||||
<span className="font-mono">{deviceInfo.secret || "N/A"}</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Device Name:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{deviceInfo.device_name || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="collapse-arrow join-item collapse border border-gray-600"
|
||||
>
|
||||
<input type="checkbox" defaultChecked />
|
||||
<div className="collapse-title text-lg font-semibold">
|
||||
PyPlayready RemoteCDM info
|
||||
</div>
|
||||
<div className="collapse-content text-slate-300">
|
||||
<p>
|
||||
<strong>Security Level:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{prDeviceInfo.security_level || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Host:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{fullHost}/remotecdm/playready
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Secret:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{prDeviceInfo.secret || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Device Name:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{prDeviceInfo.device_name || "N/A"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details open className="w-full list-none mt-5">
|
||||
<summary className="text-2xl">PyWidevine RemoteCDM info</summary>
|
||||
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
|
||||
<p>
|
||||
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
|
||||
<strong>System ID:</strong> {deviceInfo.system_id}
|
||||
<br />
|
||||
<strong>Security Level:</strong> {deviceInfo.security_level}
|
||||
<br />
|
||||
<strong>Host:</strong> {fullHost}/remotecdm/widevine
|
||||
<br />
|
||||
<strong>Secret:</strong> '{deviceInfo.secret}'<br />
|
||||
<strong>Device Name:</strong> {deviceInfo.device_name}
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details open className="w-full list-none mt-5">
|
||||
<summary className="text-2xl">PyPlayready RemoteCDM info</summary>
|
||||
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
|
||||
<p>
|
||||
<strong>Security Level:</strong> {prDeviceInfo.security_level}
|
||||
<br />
|
||||
<strong>Host:</strong> {fullHost}/remotecdm/playready
|
||||
<br />
|
||||
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
|
||||
<strong>Device Name:</strong> {prDeviceInfo.device_name}
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Container from "../Container";
|
||||
import NavBar from "../NavBar";
|
||||
import MyAccount from "./MyAccount";
|
||||
import Register from "./Register";
|
||||
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
|
||||
|
||||
function Account() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/login/status", {
|
||||
method: "POST",
|
||||
credentials: "include", // Sends cookies with request
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
@ -19,19 +22,25 @@ function Account() {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`Error checking login status. Reason: ${err.message}`);
|
||||
console.error("Error checking login status:", err);
|
||||
setIsLoggedIn(false); // Assume not logged in on error
|
||||
setIsLoggedIn(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoggedIn === null) {
|
||||
return <div>Loading...</div>; // Optional loading UI
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="accountpage" className="w-full h-full flex">
|
||||
{isLoggedIn ? <MyAccount /> : <Register />}
|
||||
</div>
|
||||
<>
|
||||
<NavBar />
|
||||
<Container>
|
||||
<div id="accountpage" className="flex h-full w-full">
|
||||
{isLoggedIn ? <MyAccount /> : <Register />}
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { toast } from "sonner";
|
||||
import Container from "../Container";
|
||||
import NavBar from "../NavBar";
|
||||
|
||||
function Cache() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [cacheData, setCacheData] = useState([]);
|
||||
const [keyCount, setKeyCount] = useState(0); // New state to store the key count
|
||||
const [keyCount, setKeyCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
|
||||
const debounceTimeout = useRef(null);
|
||||
|
||||
// Fetch the key count when the component mounts
|
||||
@ -23,32 +30,40 @@ function Cache() {
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const query = event.target.value;
|
||||
setSearchQuery(query); // Update the search query
|
||||
setSearchQuery(query);
|
||||
|
||||
// Clear the previous timeout
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
|
||||
// Set a new timeout to send the API call after 1 second of no typing
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
if (query.trim() !== "") {
|
||||
sendApiCall(query); // Only call the API if the query is not empty
|
||||
} else {
|
||||
setCacheData([]); // Clear results if query is empty
|
||||
}
|
||||
}, 1000); // 1 second delay
|
||||
if (query.trim() !== "") {
|
||||
setLoading(true); // Show spinner immediately
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
sendApiCall(query);
|
||||
}, 1000);
|
||||
} else {
|
||||
setHasSearched(false); // Reset state when input is cleared
|
||||
setCacheData([]);
|
||||
}
|
||||
};
|
||||
|
||||
const sendApiCall = (text) => {
|
||||
setLoading(true);
|
||||
fetch("/api/cache/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ input: text }),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => setCacheData(data)) // Update cache data with the results
|
||||
.catch((error) => console.error("Error:", error));
|
||||
.then((data) => {
|
||||
setCacheData(data);
|
||||
setHasSearched(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
console.error("Error:", error);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -56,51 +71,71 @@ function Cache() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full overflow-y-auto p-4">
|
||||
<div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
placeholder={`Search ${keyCount} keys...`} // Dynamic placeholder
|
||||
className="lg:grow w-full border-2 border-emerald-500/25 rounded-xl h-10 self-center m-2 text-white p-1 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all duration-200 ease-in-out"
|
||||
/>
|
||||
<a
|
||||
href="/api/cache/download"
|
||||
className="bg-emerald-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-10 truncate w-full text-center flex items-center justify-center m-2"
|
||||
>
|
||||
Download Cache
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-full grow p-4 border-2 border-emerald-500/50 rounded-2xl mt-5 overflow-y-auto">
|
||||
<table className="min-w-full text-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 border border-black">PSSH</th>
|
||||
<th className="p-2 border border-black">KID</th>
|
||||
<th className="p-2 border border-black">Key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cacheData.length > 0 ? (
|
||||
cacheData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="p-2 border border-black">{item.PSSH}</td>
|
||||
<td className="p-2 border border-black">{item.KID}</td>
|
||||
<td className="p-2 border border-black">{item.Key}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="3" className="p-2 border border-black text-center">
|
||||
No data found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<NavBar />
|
||||
<Container>
|
||||
<div className="my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
|
||||
<fieldset className="fieldset w-full max-w-2xl">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
placeholder={`Search ${keyCount} keys...`}
|
||||
className="input w-full max-w-2xl font-mono"
|
||||
/>
|
||||
</fieldset>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={() => {
|
||||
window.location.href = "/api/cache/download";
|
||||
}}
|
||||
>
|
||||
<FaDownload />
|
||||
Download keys cache
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-16">
|
||||
<span className="loading loading-spinner loading-md me-2"></span>
|
||||
Searching...
|
||||
</div>
|
||||
) : cacheData.length > 0 ? (
|
||||
<div className="my-4 flex justify-center">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th className="text-center">PSSH</th>
|
||||
<th className="text-center">key ID:key pair</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cacheData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<th>{index + 1}</th>
|
||||
<td className="font-mono">{item.PSSH}</td>
|
||||
<td className="font-mono">
|
||||
{item.KID}:{item.Key}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : hasSearched ? (
|
||||
<div className="flex justify-center py-16">
|
||||
<div className="text-center">No data found in the database</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center py-16">
|
||||
<div className="text-center">Enter a search query to see results</div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { readTextFromClipboard } from "../Functions/ParseChallenge";
|
||||
import NavBar from "../NavBar";
|
||||
import Container from "../Container";
|
||||
import { toast } from "sonner";
|
||||
import { IoInformationCircleOutline } from "react-icons/io5";
|
||||
|
||||
function HomePage() {
|
||||
const [pssh, setPssh] = useState("");
|
||||
@ -58,7 +62,7 @@ function HomePage() {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during decryption request:", error);
|
||||
setMessage("Error: Unable to process request.");
|
||||
setMessage(`Error: Unable to process request. Reason: ${error.message}`);
|
||||
setIsVisible(true);
|
||||
});
|
||||
};
|
||||
@ -67,14 +71,15 @@ function HomePage() {
|
||||
event.preventDefault();
|
||||
if (messageRef.current) {
|
||||
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
|
||||
toast.success("Copied to clipboard");
|
||||
navigator.clipboard.writeText(textToCopy).catch((err) => {
|
||||
alert("Failed to copy!");
|
||||
toast.error(`Failed to copy. Reason: ${err.message}`);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchPaste = () => {
|
||||
const handleFetchPaste = (event) => {
|
||||
event.preventDefault();
|
||||
readTextFromClipboard()
|
||||
.then(() => {
|
||||
@ -84,7 +89,8 @@ function HomePage() {
|
||||
setData(document.getElementById("data").value);
|
||||
})
|
||||
.catch((err) => {
|
||||
alert("Failed to paste from fetch!");
|
||||
toast.error(`Failed to paste from fetch. Reason: ${err.message}`);
|
||||
console.error("Failed to paste from fetch:", err);
|
||||
});
|
||||
};
|
||||
|
||||
@ -133,138 +139,150 @@ function HomePage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
|
||||
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
|
||||
<label htmlFor="pssh" className="text-white w-8/10 self-center">
|
||||
PSSH:{" "}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="pssh"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
||||
value={pssh}
|
||||
onChange={(e) => setPssh(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="licurl" className="text-white w-8/10 self-center">
|
||||
License URL:{" "}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="licurl"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
||||
value={licurl}
|
||||
onChange={(e) => setLicurl(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="proxy" className="text-white w-8/10 self-center">
|
||||
Proxy:{" "}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="proxy"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
||||
value={proxy}
|
||||
onChange={(e) => setProxy(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="headers" className="text-white w-8/10 self-center">
|
||||
Headers:{" "}
|
||||
</label>
|
||||
<textarea
|
||||
id="headers"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
||||
value={headers}
|
||||
onChange={(e) => setHeaders(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="cookies" className="text-white w-8/10 self-center">
|
||||
Cookies:{" "}
|
||||
</label>
|
||||
<textarea
|
||||
id="cookies"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
||||
value={cookies}
|
||||
onChange={(e) => setCookies(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="data" className="text-white w-8/10 self-center">
|
||||
Data:{" "}
|
||||
</label>
|
||||
<textarea
|
||||
id="data"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
||||
value={data}
|
||||
onChange={(e) => setData(e.target.value)}
|
||||
/>
|
||||
|
||||
<NavBar />
|
||||
<Container>
|
||||
<div className="mx-auto flex w-full max-w-2xl flex-col justify-center">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">PSSH*</legend>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full font-mono"
|
||||
placeholder="Enter PSSH here"
|
||||
value={pssh}
|
||||
onChange={(e) => setPssh(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="label text-red-500">* Required</p>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">License URL*</legend>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full font-mono"
|
||||
placeholder="Enter License URL here"
|
||||
value={licurl}
|
||||
onChange={(e) => setLicurl(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="label text-red-500">* Required</p>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Proxy</legend>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full font-mono"
|
||||
placeholder="Enter Proxy here (https://example.com:8080)"
|
||||
value={proxy}
|
||||
onChange={(e) => setProxy(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">
|
||||
Headers*
|
||||
<div
|
||||
className="tooltip"
|
||||
data-tip="You can use https://curlconverter.com/python/ to paste the header values here"
|
||||
>
|
||||
<IoInformationCircleOutline className="h-5 w-5" />
|
||||
</div>
|
||||
</legend>
|
||||
<textarea
|
||||
className="textarea h-48 w-full font-mono"
|
||||
placeholder="Enter headers here (JSON format). E.g. {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}"
|
||||
value={headers}
|
||||
onChange={(e) => setHeaders(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="label text-red-500">* Required</p>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Cookies</legend>
|
||||
<textarea
|
||||
className="textarea h-48 w-full font-mono"
|
||||
placeholder="Enter cookies here (JSON format)"
|
||||
value={cookies}
|
||||
onChange={(e) => setCookies(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Data</legend>
|
||||
<textarea
|
||||
className="textarea h-48 w-full font-mono"
|
||||
placeholder="Enter data here (JSON format)"
|
||||
value={data}
|
||||
onChange={(e) => setData(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
{/* Device Selection Dropdown, only show if logged in */}
|
||||
{devices.length > 0 && (
|
||||
<>
|
||||
<label htmlFor="device" className="text-white w-8/10 self-center">
|
||||
Select Device:
|
||||
</label>
|
||||
<select
|
||||
id="device"
|
||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
|
||||
value={selectedDevice}
|
||||
onChange={(e) => setSelectedDevice(e.target.value)}
|
||||
>
|
||||
{devices.map((device, index) => (
|
||||
<option key={index} value={device}>
|
||||
{device}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Select device</legend>
|
||||
<select
|
||||
className="select w-full"
|
||||
value={selectedDevice}
|
||||
onChange={(e) => setSelectedDevice(e.target.value)}
|
||||
>
|
||||
{devices.map((device, index) => (
|
||||
<option key={index} value={device}>
|
||||
{device}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</fieldset>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
|
||||
<div className="mx-auto my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
|
||||
className="btn btn-primary btn-wide"
|
||||
onClick={handleSubmitButton}
|
||||
disabled={pssh === "" || licurl === "" || headers === ""}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-info btn-wide"
|
||||
onClick={handleFetchPaste}
|
||||
className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
|
||||
>
|
||||
Paste from fetch
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
|
||||
className="btn btn-error btn-wide"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{isVisible && (
|
||||
<div
|
||||
id="main_content"
|
||||
className="flex-col w-full h-full p-10 items-center justify-center self-center"
|
||||
>
|
||||
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
|
||||
<div className="w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5">
|
||||
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
|
||||
<p
|
||||
className="w-full grow pt-10 break-words overflow-y-auto"
|
||||
ref={messageRef}
|
||||
dangerouslySetInnerHTML={{ __html: message }}
|
||||
/>
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
|
||||
<button
|
||||
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
Copy Results
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isVisible && (
|
||||
<>
|
||||
<div className="mx-auto my-4 flex w-full max-w-2xl flex-col justify-center">
|
||||
<div className="card bg-base-100 card-lg border border-gray-500 shadow-sm">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Result</h2>
|
||||
<div className="divider"></div>
|
||||
<p
|
||||
className="w-full grow overflow-y-auto font-mono break-words"
|
||||
ref={messageRef}
|
||||
dangerouslySetInnerHTML={{ __html: message }}
|
||||
/>
|
||||
<div ref={bottomRef} />
|
||||
<div
|
||||
className="card-actions mt-4 justify-end"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<button className="btn btn-success">Copy results</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import axios from "axios";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Container from "../Container";
|
||||
|
||||
function MyAccount() {
|
||||
const [wvList, setWvList] = useState([]);
|
||||
@ -21,6 +23,7 @@ function MyAccount() {
|
||||
setUsername(response.data.Styled_Username || "");
|
||||
setApiKey(response.data.API_Key || "");
|
||||
} catch (err) {
|
||||
toast.error(`Failed to fetch user info. Reason: ${err.message}`);
|
||||
console.error("Failed to fetch user info", err);
|
||||
}
|
||||
};
|
||||
@ -43,7 +46,7 @@ function MyAccount() {
|
||||
(cdmType === "PR" && extension !== "prd") ||
|
||||
(cdmType === "WV" && extension !== "wvd")
|
||||
) {
|
||||
alert(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
|
||||
toast.error(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -55,9 +58,10 @@ function MyAccount() {
|
||||
await axios.post(`/upload/${cdmType}`, formData);
|
||||
await fetchUserInfo(); // Refresh list after upload
|
||||
} catch (err) {
|
||||
toast.error(`Upload failed. Reason: ${err.message}`);
|
||||
console.error("Upload failed", err);
|
||||
alert("Upload failed");
|
||||
} finally {
|
||||
toast.success(`${cdmType} CDM uploaded successfully`);
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
@ -66,17 +70,18 @@ function MyAccount() {
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await axios.post("/logout");
|
||||
toast.success("Logged out successfully. Reloading page...");
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
toast.error(`Logout failed. Reason: ${error.message}`);
|
||||
console.error("Logout failed:", error);
|
||||
alert("Logout failed!");
|
||||
}
|
||||
};
|
||||
|
||||
// Handle change password
|
||||
const handleChangePassword = async () => {
|
||||
if (passwordError || password === "") {
|
||||
alert("Please enter a valid password.");
|
||||
toast.error("Please enter a valid password");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -86,16 +91,16 @@ function MyAccount() {
|
||||
});
|
||||
|
||||
if (response.data.message === "True") {
|
||||
alert("Password changed successfully.");
|
||||
toast.success("Password changed successfully");
|
||||
setPassword("");
|
||||
} else {
|
||||
alert("Failed to change password.");
|
||||
toast.error("Failed to change password");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data?.message === "Invalid password format") {
|
||||
alert("Password format is invalid. Please try again.");
|
||||
toast.error("Password format is invalid. Please try again.");
|
||||
} else {
|
||||
alert("Error occurred while changing password.");
|
||||
toast.error("Error occurred while changing password");
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -103,7 +108,7 @@ function MyAccount() {
|
||||
// Handle change API key
|
||||
const handleChangeApiKey = async () => {
|
||||
if (apiKeyError || newApiKey === "") {
|
||||
alert("Please enter a valid API key.");
|
||||
toast.error("Please enter a valid API key");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -112,181 +117,201 @@ function MyAccount() {
|
||||
new_api_key: newApiKey,
|
||||
});
|
||||
if (response.data.message === "True") {
|
||||
alert("API key changed successfully.");
|
||||
toast.success("API key changed successfully");
|
||||
setApiKey(newApiKey);
|
||||
setNewApiKey("");
|
||||
} else {
|
||||
alert("Failed to change API key.");
|
||||
toast.error("Failed to change API key");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error occurred while changing API key.");
|
||||
toast.error("Error occurred while changing API key");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="myaccount"
|
||||
className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4"
|
||||
>
|
||||
<div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto">
|
||||
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
|
||||
{username ? `${username}` : "My Account"}
|
||||
</h1>
|
||||
<>
|
||||
<Container>
|
||||
<div className="flex flex-col gap-4 p-4 lg:flex-row">
|
||||
{/* Left Panel - Account Settings */}
|
||||
<div className="w-full lg:w-96">
|
||||
<div className="card bg-base-200 shadow-xl">
|
||||
<div className="card-body">
|
||||
<p className="text-center text-sm">Username:</p>
|
||||
<h2 className="card-title justify-center text-center font-bold">
|
||||
{username}
|
||||
</h2>
|
||||
|
||||
{/* API Key Section */}
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<label htmlFor="apiKey" className="text-white font-semibold mb-1">
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
id="apiKey"
|
||||
type="text"
|
||||
value={apiKey}
|
||||
readOnly
|
||||
className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
||||
/>
|
||||
<div className="divider"></div>
|
||||
|
||||
{/* New API Key Section */}
|
||||
<label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1">
|
||||
New API Key
|
||||
</label>
|
||||
<input
|
||||
id="newApiKey"
|
||||
type="text"
|
||||
value={newApiKey}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const isValid = /^[^\s]+$/.test(value); // No spaces
|
||||
if (!isValid) {
|
||||
setApiKeyError("API key must not contain spaces.");
|
||||
} else {
|
||||
setApiKeyError("");
|
||||
}
|
||||
setNewApiKey(value);
|
||||
}}
|
||||
placeholder="Enter new API key"
|
||||
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
||||
/>
|
||||
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
|
||||
<button
|
||||
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
||||
onClick={handleChangeApiKey}
|
||||
>
|
||||
Change API Key
|
||||
</button>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base" htmlFor="apiKey">
|
||||
API Key
|
||||
</legend>
|
||||
<input
|
||||
name="apiKey"
|
||||
type="text"
|
||||
value={apiKey}
|
||||
readOnly
|
||||
className="input input-bordered text-center"
|
||||
/>
|
||||
|
||||
{/* Change Password Section */}
|
||||
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1">
|
||||
Change Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const isValid =
|
||||
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value);
|
||||
if (!isValid) {
|
||||
setPasswordError(
|
||||
"Password must not contain spaces or invalid characters."
|
||||
);
|
||||
} else {
|
||||
setPasswordError("");
|
||||
}
|
||||
setPassword(value);
|
||||
}}
|
||||
placeholder="New Password"
|
||||
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
||||
/>
|
||||
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>}
|
||||
<button
|
||||
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
||||
onClick={handleChangePassword}
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
<legend
|
||||
className="fieldset-legend text-base"
|
||||
htmlFor="newApiKey"
|
||||
>
|
||||
New API Key
|
||||
</legend>
|
||||
<input
|
||||
name="newApiKey"
|
||||
type="text"
|
||||
value={newApiKey}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const isValid = /^[^\s]+$/.test(value);
|
||||
if (!isValid) {
|
||||
setApiKeyError("API key must not contain spaces");
|
||||
} else {
|
||||
setApiKeyError("");
|
||||
}
|
||||
setNewApiKey(value);
|
||||
}}
|
||||
placeholder="Enter new API key"
|
||||
className="input input-bordered"
|
||||
/>
|
||||
{apiKeyError && (
|
||||
<p className="label text-error">{apiKeyError}</p>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary btn-block mt-2"
|
||||
onClick={handleChangeApiKey}
|
||||
>
|
||||
Change API key
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
<fieldset className="fieldset">
|
||||
<legend
|
||||
className="fieldset-legend text-base"
|
||||
htmlFor="passwordChange"
|
||||
>
|
||||
Change password
|
||||
</legend>
|
||||
<input
|
||||
name="passwordChange"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const isValid =
|
||||
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(
|
||||
value
|
||||
);
|
||||
if (!isValid) {
|
||||
setPasswordError("Invalid password characters");
|
||||
} else {
|
||||
setPasswordError("");
|
||||
}
|
||||
setPassword(value);
|
||||
}}
|
||||
placeholder="New password"
|
||||
className="input input-bordered"
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="label text-error">{passwordError}</p>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary btn-block mt-2"
|
||||
onClick={handleChangePassword}
|
||||
>
|
||||
Change password
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
|
||||
{/* Widevine Section */}
|
||||
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
|
||||
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">
|
||||
Widevine CDMs
|
||||
</h1>
|
||||
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
|
||||
{wvList.length === 0 ? (
|
||||
<div className="text-white text-center font-bold">
|
||||
No Widevine CDMs uploaded.
|
||||
<div className="divider"></div>
|
||||
|
||||
<button className="btn btn-error mt-auto" onClick={handleLogout}>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
wvList.map((filename, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`text-center font-bold text-white p-2 rounded ${
|
||||
i % 2 === 0 ? "bg-black/30" : "bg-black/60"
|
||||
}`}
|
||||
>
|
||||
{filename}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
|
||||
{uploading ? "Uploading..." : "Upload CDM"}
|
||||
<input
|
||||
type="file"
|
||||
accept=".wvd"
|
||||
hidden
|
||||
onChange={(e) => handleUpload(e, "WV")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Playready Section */}
|
||||
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto">
|
||||
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">
|
||||
Playready CDMs
|
||||
</h1>
|
||||
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
|
||||
{prList.length === 0 ? (
|
||||
<div className="text-white text-center font-bold">
|
||||
No Playready CDMs uploaded.
|
||||
</div>
|
||||
) : (
|
||||
prList.map((filename, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`text-center font-bold text-white p-2 rounded ${
|
||||
i % 2 === 0 ? "bg-black/30" : "bg-black/60"
|
||||
}`}
|
||||
>
|
||||
{filename}
|
||||
{/* Right Panel - CDM Uploads */}
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{/* Widevine CDM */}
|
||||
<div className="card bg-base-200 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Widevine CDMs</h2>
|
||||
<div className="divider"></div>
|
||||
<div className="max-h-60 space-y-2 overflow-y-auto">
|
||||
{wvList.length === 0 ? (
|
||||
<div className="text-center text-sm">
|
||||
No Widevine CDMs uploaded.
|
||||
</div>
|
||||
) : (
|
||||
wvList.map((filename, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`rounded px-2 py-1 text-sm ${
|
||||
i % 2 === 0 ? "bg-base-100" : "bg-base-300"
|
||||
}`}
|
||||
>
|
||||
{filename}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<label className="btn btn-accent mt-4">
|
||||
{uploading ? "Uploading..." : "Upload CDM"}
|
||||
<input
|
||||
type="file"
|
||||
accept=".wvd"
|
||||
hidden
|
||||
onChange={(e) => handleUpload(e, "WV")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PlayReady CDM */}
|
||||
<div className="card bg-base-200 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">PlayReady CDMs</h2>
|
||||
<div className="divider"></div>
|
||||
<div className="max-h-60 space-y-2 overflow-y-auto">
|
||||
{prList.length === 0 ? (
|
||||
<div className="text-center text-sm">
|
||||
No PlayReady CDMs uploaded.
|
||||
</div>
|
||||
) : (
|
||||
prList.map((filename, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`rounded px-2 py-1 text-sm ${
|
||||
i % 2 === 0 ? "bg-base-100" : "bg-base-300"
|
||||
}`}
|
||||
>
|
||||
{filename}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<label className="btn btn-accent mt-4">
|
||||
{uploading ? "Uploading..." : "Upload CDM"}
|
||||
<input
|
||||
type="file"
|
||||
accept=".prd"
|
||||
hidden
|
||||
onChange={(e) => handleUpload(e, "PR")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
|
||||
{uploading ? "Uploading..." : "Upload CDM"}
|
||||
<input
|
||||
type="file"
|
||||
accept=".prd"
|
||||
hidden
|
||||
onChange={(e) => handleUpload(e, "PR")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,118 +1,146 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { IoIosLogIn } from "react-icons/io";
|
||||
import { PiUserCirclePlus } from "react-icons/pi";
|
||||
|
||||
function Register() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
|
||||
// Validation functions
|
||||
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
|
||||
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [tab, setTab] = useState("login"); // 'login' or 'register'
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Register | CDRM-Project";
|
||||
}, []);
|
||||
|
||||
const handleRegister = async () => {
|
||||
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
|
||||
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateUsername(username)) {
|
||||
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
||||
toast.error("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
||||
return;
|
||||
}
|
||||
if (!validatePassword(password)) {
|
||||
setStatus("Invalid password. Spaces are not allowed.");
|
||||
toast.error("Invalid password. Spaces are not allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.message) {
|
||||
setStatus(data.message);
|
||||
} else if (data.error) {
|
||||
setStatus(data.error);
|
||||
if (tab === "register") {
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus("An error occurred while registering.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!validateUsername(username)) {
|
||||
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
||||
return;
|
||||
}
|
||||
if (!validatePassword(password)) {
|
||||
setStatus("Invalid password. Spaces are not allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include", // Important to send cookies
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.message) {
|
||||
// Successful login - reload the page to trigger Account check
|
||||
window.location.reload();
|
||||
} else if (data.error) {
|
||||
setStatus(data.error);
|
||||
try {
|
||||
const res = await fetch("/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.message) {
|
||||
toast.success(data.message);
|
||||
} else {
|
||||
toast.error(data.error || "Unknown error");
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`Register error: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch("/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.message) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(data.error || "Login failed");
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`Login error: ${err.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus("An error occurred while logging in.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full items-center justify-center p-4">
|
||||
<div className="flex flex-col w-full h-full lg:w-1/2 lg:h-96 border-2 border-yellow-500/50 rounded-2xl p-4 overflow-x-auto justify-center items-center">
|
||||
<div className="flex flex-col w-full">
|
||||
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">
|
||||
Username:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Username"
|
||||
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
|
||||
/>
|
||||
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">
|
||||
Password:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
|
||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div className="mx-auto">
|
||||
{/* Tabs */}
|
||||
<div className="tabs tabs-box justify-center">
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
|
||||
className={`tab ${tab === "login" ? "tab-active" : ""}`}
|
||||
onClick={() => setTab("login")}
|
||||
>
|
||||
Login
|
||||
<IoIosLogIn className="h-6 w-6 me-1"/>
|
||||
Sign in
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRegister}
|
||||
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
|
||||
className={`tab ${tab === "register" ? "tab-active" : ""}`}
|
||||
onClick={() => setTab("register")}
|
||||
>
|
||||
<PiUserCirclePlus className="h-6 w-6 me-1" />
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
{status && <p className="text-sm text-white mt-4 p-4">{status}</p>}
|
||||
<h2 className="mt-10 text-center text-2xl font-bold tracking-tight">
|
||||
{tab === "login" ? "Sign in" : "Register"}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-10 w-full max-w-xl">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Username</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Password</legend>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{tab === "register" && (
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Confirm password</legend>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="Confirm your password"
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-primary btn-block">
|
||||
{tab === "login" ? "Sign in" : "Register"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,27 +1,23 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import shaka from "shaka-player";
|
||||
import { toast } from "sonner";
|
||||
import Container from "../Container";
|
||||
import NavBar from "../NavBar";
|
||||
|
||||
function TestPlayer() {
|
||||
const [mpdUrl, setMpdUrl] = useState(""); // State to hold the MPD URL
|
||||
const [kids, setKids] = useState(""); // State to hold KIDs (separated by line breaks)
|
||||
const [keys, setKeys] = useState(""); // State to hold Keys (separated by line breaks)
|
||||
const [headers, setHeaders] = useState(""); // State to hold request headers
|
||||
const [mpdUrl, setMpdUrl] = useState("");
|
||||
const [headers, setHeaders] = useState("");
|
||||
const [keyPairs, setKeyPairs] = useState(""); // "kid:key" per line
|
||||
|
||||
const videoRef = useRef(null); // Ref for the video element
|
||||
const playerRef = useRef(null); // Ref for Shaka Player instance
|
||||
const videoRef = useRef(null);
|
||||
const playerRef = useRef(null);
|
||||
|
||||
// Function to update the MPD URL state
|
||||
const handleInputChange = (event) => {
|
||||
setMpdUrl(event.target.value);
|
||||
};
|
||||
|
||||
// Function to update KIDs and Keys
|
||||
const handleKidsChange = (event) => {
|
||||
setKids(event.target.value);
|
||||
};
|
||||
|
||||
const handleKeysChange = (event) => {
|
||||
setKeys(event.target.value);
|
||||
const handleKeyPairsChange = (event) => {
|
||||
setKeyPairs(event.target.value);
|
||||
};
|
||||
|
||||
const handleHeadersChange = (event) => {
|
||||
@ -29,31 +25,37 @@ function TestPlayer() {
|
||||
};
|
||||
|
||||
// Function to initialize Shaka Player
|
||||
const initializePlayer = () => {
|
||||
if (videoRef.current) {
|
||||
// Initialize Shaka Player only if it's not already initialized
|
||||
if (!playerRef.current) {
|
||||
const player = new shaka.Player(videoRef.current);
|
||||
playerRef.current = player;
|
||||
const initializePlayer = async () => {
|
||||
if (videoRef.current && !playerRef.current) {
|
||||
const player = new shaka.Player(); // no mediaElement
|
||||
await player.attach(videoRef.current); // attach later
|
||||
playerRef.current = player;
|
||||
|
||||
// Add error listener
|
||||
player.addEventListener("error", (event) => {
|
||||
console.error("Error code", event.detail.code, "object", event.detail);
|
||||
});
|
||||
}
|
||||
player.addEventListener("error", (event) => {
|
||||
toast.error(`Error code ${event.detail.code}: ${event.detail.message}`);
|
||||
console.error("Error code", event.detail.code, "object", event.detail);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Function to handle submit and configure player with DRM keys and headers
|
||||
const handleSubmit = () => {
|
||||
if (mpdUrl && kids && keys) {
|
||||
// Split the KIDs and Keys by new lines
|
||||
const kidsArray = kids.split("\n").map((k) => k.trim());
|
||||
const keysArray = keys.split("\n").map((k) => k.trim());
|
||||
if (mpdUrl && keyPairs) {
|
||||
// Parse KID:KEY pairs
|
||||
const lines = keyPairs
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const clearKeys = {};
|
||||
|
||||
if (kidsArray.length !== keysArray.length) {
|
||||
console.error("The number of KIDs and Keys must be the same.");
|
||||
return;
|
||||
for (const line of lines) {
|
||||
const [kid, key] = line.split(":").map((part) => part.trim());
|
||||
if (!kid || !key) {
|
||||
toast.error(`Invalid line (expected format keyId:key) at line "${line}"`);
|
||||
console.error(`Invalid line (expected format keyId:key) at line "${line}"`);
|
||||
return;
|
||||
}
|
||||
clearKeys[kid] = key;
|
||||
}
|
||||
|
||||
// Initialize Shaka Player only when the submit button is pressed
|
||||
@ -62,15 +64,10 @@ function TestPlayer() {
|
||||
// Widevine DRM configuration with the provided KIDs and Keys
|
||||
const config = {
|
||||
drm: {
|
||||
clearKeys: {},
|
||||
clearKeys: clearKeys,
|
||||
},
|
||||
};
|
||||
|
||||
// Map KIDs to Keys
|
||||
kidsArray.forEach((kid, index) => {
|
||||
config.drm.clearKeys[kid] = keysArray[index];
|
||||
});
|
||||
|
||||
console.log("Configuring player with the following DRM config and headers:", config);
|
||||
|
||||
// Configure the player with ClearKey DRM and custom headers
|
||||
@ -81,12 +78,15 @@ function TestPlayer() {
|
||||
.load(mpdUrl)
|
||||
.then(() => {
|
||||
console.log("Video loaded");
|
||||
toast.success("Video successfully loaded");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error loading the video. Reason: ${error.message}`);
|
||||
console.error("Error loading the video", error);
|
||||
});
|
||||
} else {
|
||||
console.error("MPD URL, KIDs, and Keys are required.");
|
||||
toast.error("Manifest URL and key pairs are required");
|
||||
console.error("Manifest URL and key pairs are required.");
|
||||
}
|
||||
};
|
||||
|
||||
@ -113,39 +113,65 @@ function TestPlayer() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full p-4">
|
||||
<div className="w-full flex flex-col">
|
||||
<video ref={videoRef} width="100%" height="auto" controls className="h-96" />
|
||||
<input
|
||||
type="text"
|
||||
value={mpdUrl}
|
||||
onChange={handleInputChange}
|
||||
placeholder="MPD URL"
|
||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="KIDs (one per line)"
|
||||
value={kids}
|
||||
onChange={handleKidsChange}
|
||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Keys (one per line)"
|
||||
value={keys}
|
||||
onChange={handleKeysChange}
|
||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Headers (one per line)"
|
||||
value={headers}
|
||||
onChange={handleHeadersChange}
|
||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
||||
/>
|
||||
<button onClick={handleSubmit} className="mt-4 p-2 bg-blue-500 text-white rounded">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<NavBar />
|
||||
<Container>
|
||||
<div className="flex w-full flex-col items-center justify-center py-8">
|
||||
<div className="flex w-full flex-col items-center lg:flex-row lg:items-start lg:gap-4">
|
||||
{/* Video Section */}
|
||||
<div className="w-full lg:w-1/2">
|
||||
<video
|
||||
ref={videoRef}
|
||||
width="100%"
|
||||
height="auto"
|
||||
controls
|
||||
className="aspect-video max-h-96 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Inputs Section */}
|
||||
<div className="mt-4 flex w-full flex-col items-center lg:mt-0 lg:w-1/2">
|
||||
<fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend text-base">Manifest URL*</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={mpdUrl}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter manifest URL here"
|
||||
className="input w-full font-mono"
|
||||
/>
|
||||
<p className="label text-red-500">* Required</p>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend text-base">Key pairs*</legend>
|
||||
<textarea
|
||||
placeholder="keyId:key pair (one per line)"
|
||||
value={keyPairs}
|
||||
onChange={handleKeyPairsChange}
|
||||
className="textarea w-full font-mono"
|
||||
/>
|
||||
<p className="label text-red-500">* Required</p>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend text-base">Headers</legend>
|
||||
<textarea
|
||||
placeholder="Headers (one per line)"
|
||||
value={headers}
|
||||
onChange={handleHeadersChange}
|
||||
className="textarea w-full font-mono"
|
||||
/>
|
||||
</fieldset>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="btn btn-primary btn-wide my-4"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,31 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dim";
|
||||
default: true;
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--depth: 0;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans:
|
||||
"Inter", system-ui, -apple-system, Roboto, "Segoe UI", "Helvetica Neue", "Noto Sans",
|
||||
Oxygen, Ubuntu, Cantarell, "Open Sans", Arial, sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
/* Force Sonner toast to use Inter first */
|
||||
[data-sonner-toast],
|
||||
.sonner-toast,
|
||||
:where([data-sonner-toast]) :where([data-title]) :where([data-description]) {
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
|
@ -1,13 +1,22 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import { Toaster } from "sonner";
|
||||
import App from "./App.jsx";
|
||||
import "./assets/fonts/font-face.css";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
<Toaster
|
||||
richColors
|
||||
className="flex justify-center"
|
||||
position="bottom-center"
|
||||
duration="7000"
|
||||
theme="dark"
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user