forked from tpd94/CDRM-Extension
UI overhaul
This commit is contained in:
parent
37334ae389
commit
d419af0fb9
9
frontend/.prettierrc.json
Normal file
9
frontend/.prettierrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
137
frontend/package-lock.json
generated
137
frontend/package-lock.json
generated
@ -11,7 +11,9 @@
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -20,10 +22,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"
|
||||
}
|
||||
},
|
||||
@ -2093,6 +2097,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.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@ -3204,6 +3218,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/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -3235,6 +3353,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",
|
||||
@ -3377,6 +3504,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",
|
||||
|
@ -13,7 +13,9 @@
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,41 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { HashRouter as Router, Routes, Route, Navigate } from "react-router-dom";
|
||||
import TopNav from "./components/topnav";
|
||||
import SideNav from "./components/sidenav";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Navigate, Route, HashRouter as Router, Routes } from "react-router-dom";
|
||||
import About from "./components/about";
|
||||
import Container from "./components/container";
|
||||
import Results from "./components/results";
|
||||
import Settings from "./components/settings";
|
||||
import TabNavigation from "./components/tabnavigation";
|
||||
|
||||
function App() {
|
||||
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
|
||||
const [validConfig, setValidConfig] = useState(null); // null = loading
|
||||
const App = () => {
|
||||
const [validConfig, setValidConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get("valid_config", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error reading valid_config:", chrome.runtime.lastError);
|
||||
setValidConfig(false); // fallback
|
||||
} else {
|
||||
setValidConfig(result.valid_config === true);
|
||||
}
|
||||
});
|
||||
// Fix: Access chrome API properly for browser extensions
|
||||
if (typeof chrome !== "undefined" && chrome.storage) {
|
||||
chrome.storage.local.get("valid_config", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error reading valid_config:", chrome.runtime.lastError);
|
||||
setValidConfig(false);
|
||||
} else {
|
||||
setValidConfig(result.valid_config === true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback for development/testing
|
||||
setValidConfig(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleConfigSaved = () => {
|
||||
setValidConfig(true);
|
||||
// Navigate to main tab after config is saved
|
||||
window.location.hash = "#/results";
|
||||
};
|
||||
|
||||
if (validConfig === null) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-black text-white">
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<span className="loading loading-spinner loading-md ms-2"></span>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
@ -30,20 +43,16 @@ function App() {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-w-full min-h-full w-full h-full flex flex-grow bg-black/95 flex-col relative">
|
||||
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black overflow-x-hidden">
|
||||
<TopNav onMenuClick={() => setIsSideNavOpen(true)} />
|
||||
</div>
|
||||
|
||||
<div id="currentpagecontainer" className="w-full grow overflow-y-auto">
|
||||
<div className="flex h-screen flex-col py-4">
|
||||
<Container>
|
||||
<TabNavigation validConfig={validConfig} />
|
||||
<div className="divider"></div>
|
||||
<Routes>
|
||||
{!validConfig ? (
|
||||
<>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<Settings onConfigSaved={() => setValidConfig(true)} />
|
||||
}
|
||||
element={<Settings onConfigSaved={handleConfigSaved} />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/settings" replace />} />
|
||||
</>
|
||||
@ -52,21 +61,14 @@ function App() {
|
||||
<Route path="/" element={<Navigate to="/results" replace />} />
|
||||
<Route path="/results" element={<Results />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full h-full z-50 bg-black transform transition-transform duration-300 ease-in-out ${
|
||||
isSideNavOpen ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<SideNav onClose={() => setIsSideNavOpen(false)} />
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
BIN
frontend/src/assets/fonts/InterVariable-Italic.woff2
Normal file
BIN
frontend/src/assets/fonts/InterVariable-Italic.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/InterVariable.woff2
Normal file
BIN
frontend/src/assets/fonts/InterVariable.woff2
Normal file
Binary file not shown.
15
frontend/src/assets/fonts/font-face.css
Normal file
15
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;
|
||||
}
|
57
frontend/src/components/about.jsx
Normal file
57
frontend/src/components/about.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { FaDiscord, FaTelegram } from "react-icons/fa";
|
||||
import { SiGitea } from "react-icons/si";
|
||||
|
||||
const AboutPage = () => {
|
||||
const socialLinks = [
|
||||
{
|
||||
name: "Discord",
|
||||
icon: <FaDiscord className="text-4xl" />,
|
||||
url: "https://discord.cdrm-project.com/",
|
||||
description: "Join our Discord community",
|
||||
color: "hover:text-indigo-400",
|
||||
},
|
||||
{
|
||||
name: "Telegram",
|
||||
icon: <FaTelegram className="text-4xl" />,
|
||||
url: "https://telegram.cdrm-project.com/",
|
||||
description: "Follow us on Telegram",
|
||||
color: "hover:text-sky-400",
|
||||
},
|
||||
{
|
||||
name: "Gitea",
|
||||
icon: <SiGitea className="text-4xl" />,
|
||||
url: "https://cdm-project.com/tpd94/cdrm-project",
|
||||
description: "Check out our code",
|
||||
color: "hover:text-lime-400",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col items-center justify-center p-6">
|
||||
<div className="mb-8 text-center">
|
||||
<h2 className="mb-2 text-3xl font-bold">Connect with us</h2>
|
||||
<p className="text-base-content/70 text-lg">Join our community and stay updated</p>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full max-w-4xl grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{socialLinks.map((link) => (
|
||||
<a
|
||||
key={link.name}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`card bg-base-200 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl ${link.color}`}
|
||||
>
|
||||
<div className="card-body items-center text-center">
|
||||
<div className="mb-2">{link.icon}</div>
|
||||
<h3 className="card-title text-xl font-semibold">{link.name}</h3>
|
||||
<p className="text-base-content/70">{link.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutPage;
|
9
frontend/src/components/container.jsx
Normal file
9
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;
|
72
frontend/src/components/injectionmenu.jsx
Normal file
72
frontend/src/components/injectionmenu.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const InjectionMenu = () => {
|
||||
const [injectionType, setInjectionType] = useState("LICENSE");
|
||||
const [drmOverride, setDrmOverride] = useState("DISABLED");
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
|
||||
if (result.injection_type !== undefined) {
|
||||
setInjectionType(result.injection_type);
|
||||
}
|
||||
if (result.drm_override !== undefined) {
|
||||
setDrmOverride(result.drm_override);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleInjectionTypeChange = (type) => {
|
||||
chrome.storage.local.set({ injection_type: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating injection_type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setInjectionType(type);
|
||||
console.log(`Injection type updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrmOverrideChange = (type) => {
|
||||
chrome.storage.local.set({ drm_override: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating drm_override:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setDrmOverride(type);
|
||||
console.log(`DRM Override updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-2 ml-auto flex h-full flex-row items-center justify-center">
|
||||
<p className="mr-2 p-2 text-lg text-nowrap">Injection type:</p>
|
||||
<div role="tablist" className="tabs tabs-border">
|
||||
<a
|
||||
role="tab"
|
||||
className={`tab ${injectionType === "LICENSE" ? "tab-active font-semibold" : ""}`}
|
||||
onClick={() => handleInjectionTypeChange("LICENSE")}
|
||||
>
|
||||
License
|
||||
</a>
|
||||
<a
|
||||
role="tab"
|
||||
className={`tab ${injectionType === "EME" ? "tab-active font-semibold" : ""}`}
|
||||
onClick={() => handleInjectionTypeChange("EME")}
|
||||
>
|
||||
EME
|
||||
</a>
|
||||
<a
|
||||
role="tab"
|
||||
className={`tab ${injectionType === "DISABLED" ? "tab-active font-semibold" : ""}`}
|
||||
onClick={() => handleInjectionTypeChange("DISABLED")}
|
||||
>
|
||||
Disabled
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InjectionMenu;
|
@ -1,6 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IoCameraOutline, IoCopyOutline, IoSaveOutline } from "react-icons/io5";
|
||||
import { toast } from "sonner";
|
||||
import InjectionMenu from "./injectionmenu";
|
||||
|
||||
function Results() {
|
||||
const Results = () => {
|
||||
const [drmType, setDrmType] = useState("");
|
||||
const [pssh, setPssh] = useState("");
|
||||
const [licenseUrl, setLicenseUrl] = useState("");
|
||||
@ -118,6 +121,11 @@ function Results() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = (text, label) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success(`Copied ${label} to clipboard`);
|
||||
};
|
||||
|
||||
// Check if current tab is YouTube
|
||||
const isYouTube = () => {
|
||||
return currentTabUrl.includes("youtube.com") || currentTabUrl.includes("youtu.be");
|
||||
@ -177,74 +185,137 @@ function Results() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full grow flex h-full overflow-y-auto overflow-x-auto flex-col text-white p-4">
|
||||
<button
|
||||
onClick={handleCapture}
|
||||
className="w-full h-10 bg-sky-500 rounded-md p-2 mt-2 text-white cursor-pointer font-bold hover:bg-sky-600"
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-4">
|
||||
<InjectionMenu />
|
||||
<button onClick={handleCapture} className="btn btn-primary">
|
||||
<IoCameraOutline className="h-5 w-5" />
|
||||
Capture current tab
|
||||
</button>
|
||||
|
||||
<p className="text-2xl mt-5">DRM Type</p>
|
||||
<input
|
||||
type="text"
|
||||
value={drmType}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">DRM Type</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={drmType}
|
||||
className="input w-full font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<p className="text-2xl mt-5">Manifest URL</p>
|
||||
<input
|
||||
type="text"
|
||||
value={getManifestDisplayValue()}
|
||||
className={`w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 font-mono ${
|
||||
isYouTube() && !manifestUrl ? "text-yellow-400" : "text-white"
|
||||
}`}
|
||||
placeholder={getManifestPlaceholder()}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">PSSH</p>
|
||||
<input
|
||||
type="text"
|
||||
value={pssh}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">License URL</p>
|
||||
<input
|
||||
type="text"
|
||||
value={licenseUrl}
|
||||
className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<p className="text-2xl mt-5">Keys</p>
|
||||
<div className="w-full min-h-64 h-64 flex items-center justify-center text-center overflow-y-auto bg-slate-800/50 rounded-md p-2 mt-2 text-white whitespace-pre-line font-mono">
|
||||
{Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0 ? (
|
||||
keys
|
||||
.filter((k) => k.type !== "SIGNING")
|
||||
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
|
||||
.join("\n")
|
||||
) : (
|
||||
<span className="text-gray-400">[Not available]</span>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Manifest URL</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={getManifestDisplayValue()}
|
||||
className={`input w-full font-mono ${
|
||||
isYouTube() && !manifestUrl ? "text-yellow-400" : ""
|
||||
}`}
|
||||
placeholder={getManifestPlaceholder()}
|
||||
disabled
|
||||
/>
|
||||
{!isYouTube() && manifestUrl && (
|
||||
<p className="label flex justify-end">
|
||||
<button
|
||||
className="btn btn-link btn-sm text-info"
|
||||
onClick={() => handleCopyToClipboard(manifestUrl, "manifest URL")}
|
||||
>
|
||||
<IoCopyOutline className="h-5 w-5" />
|
||||
Copy manifest URL
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">PSSH</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={pssh}
|
||||
className="input w-full font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
{pssh && (
|
||||
<p className="label flex justify-end">
|
||||
<button
|
||||
className="btn btn-link btn-sm text-info"
|
||||
onClick={() => handleCopyToClipboard(pssh, "PSSH")}
|
||||
>
|
||||
<IoCopyOutline className="h-5 w-5" />
|
||||
Copy PSSH
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">License URL</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={licenseUrl}
|
||||
className="input w-full font-mono"
|
||||
placeholder="[Not available]"
|
||||
disabled
|
||||
/>
|
||||
{licenseUrl && (
|
||||
<p className="label flex justify-end">
|
||||
<button
|
||||
className="btn btn-link btn-sm text-info"
|
||||
onClick={() => handleCopyToClipboard(licenseUrl, "license URL")}
|
||||
>
|
||||
<IoCopyOutline className="h-5 w-5" />
|
||||
Copy license URL
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">Keys</legend>
|
||||
<textarea
|
||||
value={
|
||||
Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0
|
||||
? keys
|
||||
.filter((k) => k.type !== "SIGNING")
|
||||
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
|
||||
.join("\n")
|
||||
: "[Not available]"
|
||||
}
|
||||
className="textarea w-full font-mono"
|
||||
disabled
|
||||
/>
|
||||
{hasData() &&
|
||||
Array.isArray(keys) &&
|
||||
keys.filter((k) => k.type !== "SIGNING").length > 0 && (
|
||||
<p className="label flex justify-end">
|
||||
<button
|
||||
className="btn btn-link btn-sm text-info"
|
||||
onClick={() =>
|
||||
handleCopyToClipboard(
|
||||
keys
|
||||
.filter((k) => k.type !== "SIGNING")
|
||||
.map((k) => `${k.key_id || k.keyId}:${k.key}`)
|
||||
.join("\n"),
|
||||
"keys"
|
||||
)
|
||||
}
|
||||
>
|
||||
<IoCopyOutline className="h-5 w-5" />
|
||||
Copy keys
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
{hasData() && (
|
||||
<button
|
||||
onClick={handleExportJSON}
|
||||
className="w-full h-10 bg-green-500 rounded-md p-2 mt-5 text-white cursor-pointer font-bold hover:bg-green-600"
|
||||
>
|
||||
<button onClick={handleExportJSON} className="btn btn-success">
|
||||
<IoSaveOutline className="h-5 w-5" />
|
||||
Export as JSON
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Results;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IoSaveOutline } from "react-icons/io5";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function Settings({ onConfigSaved }) {
|
||||
const Settings = ({ onConfigSaved }) => {
|
||||
const [instanceUrl, setInstanceUrl] = useState("");
|
||||
const [storedUrl, setStoredUrl] = useState(null);
|
||||
const [message, setMessage] = useState(null);
|
||||
const [messageType, setMessageType] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -13,6 +13,7 @@ function Settings({ onConfigSaved }) {
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get("cdrm_instance", (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
toast.error("Error fetching CDRM instance:", chrome.runtime.lastError);
|
||||
console.error("Error fetching CDRM instance:", chrome.runtime.lastError);
|
||||
} else if (result.cdrm_instance) {
|
||||
setStoredUrl(result.cdrm_instance);
|
||||
@ -23,14 +24,12 @@ function Settings({ onConfigSaved }) {
|
||||
const handleSave = async () => {
|
||||
const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
|
||||
if (!trimmedUrl) {
|
||||
setMessage("Please enter a valid URL.");
|
||||
setMessageType("error");
|
||||
toast.error("Please enter a valid URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = trimmedUrl + "/api/extension";
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
@ -43,15 +42,24 @@ function Settings({ onConfigSaved }) {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === true) {
|
||||
setMessage("Successfully connected to CDRM Instance.");
|
||||
setMessageType("success");
|
||||
toast.success("Successfully connected to a CDRM instance");
|
||||
|
||||
const widevineRes = await fetch(`${trimmedUrl}/remotecdm/widevine/deviceinfo`);
|
||||
if (!widevineRes.ok) throw new Error("Failed to fetch Widevine device info");
|
||||
if (!widevineRes.ok) {
|
||||
toast.error(
|
||||
`Failed to fetch Widevine device info. Reason: ${widevineRes.statusText}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const widevineData = await widevineRes.json();
|
||||
|
||||
const playreadyRes = await fetch(`${trimmedUrl}/remotecdm/playready/deviceinfo`);
|
||||
if (!playreadyRes.ok) throw new Error("Failed to fetch PlayReady device info");
|
||||
if (!playreadyRes.ok) {
|
||||
toast.error(
|
||||
`Failed to fetch PlayReady device info. Reason: ${playreadyRes.statusText}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const playreadyData = await playreadyRes.json();
|
||||
|
||||
chrome.storage.local.set(
|
||||
@ -79,10 +87,11 @@ function Settings({ onConfigSaved }) {
|
||||
"Error saving to chrome.storage:",
|
||||
chrome.runtime.lastError
|
||||
);
|
||||
setMessage("Error saving configuration.");
|
||||
setMessageType("error");
|
||||
toast.error(
|
||||
`Error saving configuration. Reason: ${chrome.runtime.lastError}`
|
||||
);
|
||||
} else {
|
||||
console.log("Configuration saved.");
|
||||
console.log("Configuration saved");
|
||||
setStoredUrl(trimmedUrl);
|
||||
setInstanceUrl("");
|
||||
if (onConfigSaved) onConfigSaved();
|
||||
@ -91,54 +100,56 @@ function Settings({ onConfigSaved }) {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw new Error("Invalid response from endpoint.");
|
||||
toast.error("Invalid response from endpoint.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Connection error:", err);
|
||||
setMessage("Invalid endpoint or device info could not be retrieved.");
|
||||
setMessageType("error");
|
||||
toast.error(
|
||||
`Invalid endpoint or device info could not be retrieved. Reason: ${err.message}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
|
||||
<div className="flex h-full w-full flex-col gap-4">
|
||||
{storedUrl && (
|
||||
<p className="text-gray-300 mb-2">
|
||||
Current instance: <span className="text-white font-semibold">{storedUrl}</span>
|
||||
<p className="mb-2 text-base">
|
||||
Current instance: <span className="font-mono font-semibold">{storedUrl}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mt-3 text-white">New instance URL:</p>
|
||||
<input
|
||||
type="text"
|
||||
value={instanceUrl}
|
||||
onChange={(e) => setInstanceUrl(e.target.value)}
|
||||
placeholder="https://cdrm-project.com/, http://127.0.0.1:5000/"
|
||||
className="w-full p-4 text-lg bg-gray-800 text-white border border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mt-4 font-mono"
|
||||
/>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-base">New instance URL</legend>
|
||||
<input
|
||||
type="text"
|
||||
value={instanceUrl}
|
||||
onChange={(e) => setInstanceUrl(e.target.value)}
|
||||
placeholder="https://cdrm-project.com/, http://127.0.0.1:5000/"
|
||||
className="input w-full font-mono"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className={`mt-4 p-2 font-bold ${
|
||||
loading ? "bg-blue-400" : "bg-blue-600 hover:bg-blue-700"
|
||||
} text-white rounded-md transition duration-300`}
|
||||
className="btn btn-primary btn-block"
|
||||
>
|
||||
{loading ? "Connecting..." : "Save settings"}
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm"></span> Connecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoSaveOutline className="h-5 w-5" />
|
||||
Save settings
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{message && (
|
||||
<p
|
||||
className={`mt-2 text-sm text-center ${
|
||||
messageType === "success" ? "text-green-400" : "text-red-400"
|
||||
}`}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
@ -1,48 +0,0 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
import homeIcon from "../assets/home.svg";
|
||||
import settingsIcon from "../assets/settings.svg";
|
||||
import closeIcon from "../assets/close.svg";
|
||||
|
||||
function SideNav({ onClose }) {
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black">
|
||||
<div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black">
|
||||
<button onClick={onClose} className="h-full ml-auto p-3 hover:cursor-pointer">
|
||||
<img src={closeIcon} alt="Close" className="h-full" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||
<NavLink
|
||||
to="/results"
|
||||
onClick={onClose}
|
||||
className="text-white text-2xl font-bold flex flex-row items-center border-l-white hover:border-l-1 w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
|
||||
>
|
||||
<img
|
||||
src={homeIcon}
|
||||
alt="Home"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
/>
|
||||
Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-16 flex items-center justify-center mt-2">
|
||||
<NavLink
|
||||
to="/settings"
|
||||
onClick={onClose}
|
||||
className="text-white text-2xl font-bold flex flex-row items-center hover:border-l-1 border-l-white w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
|
||||
>
|
||||
<img
|
||||
src={settingsIcon}
|
||||
alt="Settings"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
/>
|
||||
Settings
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideNav;
|
51
frontend/src/components/tabnavigation.jsx
Normal file
51
frontend/src/components/tabnavigation.jsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { IoIosInformationCircleOutline } from "react-icons/io";
|
||||
import { IoHomeOutline, IoSettingsOutline } from "react-icons/io5";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
|
||||
const TabNavigation = ({ validConfig }) => {
|
||||
const location = useLocation();
|
||||
const activeTab =
|
||||
location.pathname === "/settings"
|
||||
? "settings"
|
||||
: location.pathname === "/about"
|
||||
? "about"
|
||||
: "main";
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<div role="tablist" className="tabs tabs-box">
|
||||
<NavLink
|
||||
role="tab"
|
||||
to="/results"
|
||||
className={`tab ${!validConfig ? "cursor-not-allowed" : activeTab === "main" ? "tab-active font-semibold" : ""}`}
|
||||
onClick={(e) => {
|
||||
if (!validConfig) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IoHomeOutline className="mr-1 h-5 w-5" />
|
||||
Main
|
||||
</NavLink>
|
||||
<NavLink
|
||||
role="tab"
|
||||
to="/settings"
|
||||
className={`tab ${activeTab === "settings" ? "tab-active font-semibold" : ""}`}
|
||||
>
|
||||
<IoSettingsOutline className="mr-1 h-5 w-5" />
|
||||
Settings
|
||||
</NavLink>
|
||||
<NavLink
|
||||
role="tab"
|
||||
to="/about"
|
||||
className={`tab ${activeTab === "about" ? "tab-active font-semibold" : ""}`}
|
||||
>
|
||||
<IoIosInformationCircleOutline className="mr-1 h-5 w-5" />
|
||||
About
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigation;
|
@ -1,82 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import hamburgerIcon from "../assets/hamburger.svg";
|
||||
|
||||
function TopNav({ onMenuClick }) {
|
||||
const [injectionType, setInjectionType] = useState("LICENSE");
|
||||
const [drmOverride, setDrmOverride] = useState("DISABLED");
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
|
||||
if (result.injection_type !== undefined) {
|
||||
setInjectionType(result.injection_type);
|
||||
}
|
||||
if (result.drm_override !== undefined) {
|
||||
setDrmOverride(result.drm_override);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleInjectionTypeChange = (type) => {
|
||||
chrome.storage.local.set({ injection_type: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating injection_type:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setInjectionType(type);
|
||||
console.log(`Injection type updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrmOverrideChange = (type) => {
|
||||
chrome.storage.local.set({ drm_override: type }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error updating drm_override:", chrome.runtime.lastError);
|
||||
} else {
|
||||
setDrmOverride(type);
|
||||
console.log(`DRM Override updated to ${type}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-row overflow-x-hidden">
|
||||
<img
|
||||
src={hamburgerIcon}
|
||||
alt="Menu"
|
||||
className="h-full w-16 p-2 flex items-center cursor-pointer"
|
||||
onClick={onMenuClick}
|
||||
/>
|
||||
<div className="flex flex-row h-full justify-center items-center ml-auto mr-2">
|
||||
<p className="text-white text-lg p-2 mr-2 border-r-2 border-r-white text-nowrap">
|
||||
Injection Type:
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("LICENSE")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "LICENSE" ? "bg-sky-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
License
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("EME")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "EME" ? "bg-green-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
EME
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleInjectionTypeChange("DISABLED")}
|
||||
className={`text-white text-lg p-2 rounded-md m-1 cursor-pointer ${
|
||||
injectionType === "DISABLED" ? "bg-red-500/70" : "bg-black"
|
||||
}`}
|
||||
>
|
||||
Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopNav;
|
@ -1,10 +1,35 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dim";
|
||||
default: true;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--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-default-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-default-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-default-sans) !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
body {
|
||||
font-family: var(--font-default-sans);
|
||||
}
|
||||
|
@ -1,10 +1,19 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
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>
|
||||
<App />
|
||||
<Toaster
|
||||
richColors
|
||||
className="flex justify-center"
|
||||
position="bottom-center"
|
||||
duration="7000"
|
||||
theme="dark"
|
||||
/>
|
||||
</StrictMode>
|
||||
);
|
||||
|
@ -291,7 +291,12 @@ class RemoteCDMBase {
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (xhr.status === 200 || jsonData.status === "Success" || jsonData.message?.includes("parsed and loaded")) {
|
||||
if (
|
||||
xhr.status === 200 ||
|
||||
jsonData.status === "Success" ||
|
||||
jsonData.status === 200 ||
|
||||
jsonData.message?.includes("parsed and loaded")
|
||||
) {
|
||||
logWithPrefix("License response parsed successfully");
|
||||
return true;
|
||||
} else {
|
||||
@ -390,7 +395,7 @@ class remoteWidevineCDM extends RemoteCDMBase {
|
||||
};
|
||||
xhr.send(JSON.stringify(body));
|
||||
const jsonData = JSON.parse(xhr.responseText);
|
||||
if (xhr.status === 200 || jsonData.status === "Success") {
|
||||
if (xhr.status === 200 || jsonData.status === "Success" || jsonData.status === 200) {
|
||||
logWithPrefix("Service certificate set successfully");
|
||||
} else {
|
||||
console.error("Failed to set service certificate:", jsonData.message);
|
||||
|
Loading…
x
Reference in New Issue
Block a user