UI overhaul

This commit is contained in:
voldemort 2025-07-24 11:43:01 +07:00
parent cc3b37db1d
commit f83d22c09e
18 changed files with 1164 additions and 744 deletions

View File

@ -4,5 +4,6 @@
"semi": true,
"singleQuote": false,
"useTabs": false,
"printWidth": 100
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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>
);
}

Binary file not shown.

View 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;
}

View 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;

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);

View File

@ -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>
</>
);
}

View File

@ -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;

View File

@ -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>
);