diff --git a/panel/panel.css b/panel/panel.css
new file mode 100644
index 0000000..25a6707
--- /dev/null
+++ b/panel/panel.css
@@ -0,0 +1,145 @@
+body {
+ font-family: Arial, sans-serif;
+ background-color: #ffffff;
+ color: black;
+ width: 400px;
+}
+.toggleButton {
+ width: 24px;
+}
+select {
+ max-width: 330px;
+}
+.text-box {
+ outline: none;
+ width: 80%;
+}
+#downloader-name {
+ width: 160px;
+}
+#export {
+ margin-top: 5px;
+}
+
+/* Dark mode */
+.dark-mode {
+ background-color: #1b1b1b;
+ color: #d5d5d5;
+}
+.dark-mode fieldset {
+ border-color: #535353;
+}
+.dark-mode button,
+.dark-mode select {
+ background-color: #222;
+ color: #d5d5d5;
+}
+.dark-mode button:hover,
+.dark-mode select:hover {
+ background-color: #323232;
+}
+.dark-mode .expandableDiv {
+ background-color: #1b2027;
+}
+.dark-mode .text-box {
+ background-color: #323232;
+ color: #d5d5d5;
+}
+
+/* Switch */
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 40px;
+ height: 20px;
+ margin-left: 10px;
+}
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+/* Slider */
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ transition: 0.4s;
+ border-radius: 20px;
+}
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 14px;
+ width: 14px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ border-radius: 50%;
+}
+input:checked + .slider {
+ background-color: #2196f3;
+}
+input:checked + .slider:before {
+ transform: translateX(20px);
+}
+
+#clear {
+ width: 100%;
+ margin-bottom: 3px;
+}
+
+#type-select {
+ margin-left: auto;
+}
+
+#settings {
+ display: flex;
+}
+
+.header {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.header > * {
+ margin: 5px;
+}
+
+#remote {
+ margin: 5px;
+}
+
+.log-container {
+ display: flex;
+ justify-content: center;
+}
+.right-bound {
+ text-align: right;
+}
+.expandableDiv {
+ width: 100%;
+ overflow: hidden;
+ background-color: lightblue;
+ padding: 0;
+}
+.expandableDiv.expanded {
+ padding: 5px;
+}
+.expandableDiv.collapsed {
+ padding: 0;
+}
+.always-visible {
+ display: block;
+}
+.expanded-only {
+ display: none;
+}
+.expandableDiv.expanded .expanded-only {
+ display: block;
+}
diff --git a/panel/panel.html b/panel/panel.html
new file mode 100644
index 0000000..d2f90ca
--- /dev/null
+++ b/panel/panel.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ Playready Proxy Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/panel/panel.js b/panel/panel.js
new file mode 100644
index 0000000..bab9a32
--- /dev/null
+++ b/panel/panel.js
@@ -0,0 +1,259 @@
+import {
+ AsyncLocalStorage,
+ base64toUint8Array,
+ stringToUint8Array,
+ RemoteCDMManager,
+ SettingsManager,
+} from "../util.js";
+
+const key_container = document.getElementById("key-container");
+
+// ================ Main ================
+const enabled = document.getElementById("enabled");
+enabled.addEventListener("change", async function () {
+ await SettingsManager.setEnabled(enabled.checked);
+});
+
+const export_button = document.getElementById("export");
+export_button.addEventListener("click", async function () {
+ const logs = await AsyncLocalStorage.getStorage(null);
+ SettingsManager.downloadFile(
+ stringToUint8Array(JSON.stringify(logs)),
+ "logs.json"
+ );
+});
+
+
+// ================ Remote CDM ================
+document.getElementById("remoteInput").addEventListener("click", () => {
+ chrome.runtime.sendMessage({ type: "OPEN_PICKER" });
+ window.close();
+});
+
+const remote_remove = document.getElementById("remoteRemove");
+remote_remove.addEventListener("click", async function () {
+ await RemoteCDMManager.removeSelectedRemoteCDM();
+ remote_combobox.innerHTML = "";
+ await RemoteCDMManager.loadSetAllRemoteCDMs();
+ const selected_option =
+ remote_combobox.options[remote_combobox.selectedIndex];
+ if (selected_option) {
+ await RemoteCDMManager.saveSelectedRemoteCDM(selected_option.text);
+ } else {
+ await RemoteCDMManager.removeSelectedRemoteCDMKey();
+ }
+});
+
+const remote_download = document.getElementById("remoteDownload");
+remote_download.addEventListener("click", async function () {
+ const remote_cdm = await RemoteCDMManager.getSelectedRemoteCDM();
+ SettingsManager.downloadFile(
+ await RemoteCDMManager.loadRemoteCDM(remote_cdm),
+ remote_cdm + ".json"
+ );
+});
+
+const remote_combobox = document.getElementById("remote-combobox");
+remote_combobox.addEventListener("change", async function () {
+ await RemoteCDMManager.saveSelectedRemoteCDM(
+ remote_combobox.options[remote_combobox.selectedIndex].text
+ );
+});
+
+const local_combobox = document.getElementById("local-combobox");
+local_combobox.addEventListener("change", async function () {
+ await LocalCDMManager.saveSelectedLocalCDM(
+ local_combobox.options[local_combobox.selectedIndex].text
+ );
+});
+// ============================================
+
+// ====================== Proxy Settings ======================
+
+// Get elements
+const enableProxy = document.getElementById("enable-proxy");
+const proxyConfig = document.getElementById("proxy-config");
+const proxyUrlInput = document.getElementById("proxy-url");
+
+// Enable or disable proxy settings
+enableProxy.addEventListener("change", async function () {
+ const isProxyEnabled = enableProxy.checked;
+ if (isProxyEnabled) {
+ proxyConfig.style.display = "block";
+ } else {
+ proxyConfig.style.display = "none";
+ await SettingsManager.saveProxyConfig(""); // Clear proxy setting if disabled
+ }
+ await SettingsManager.setProxyEnabled(isProxyEnabled);
+});
+
+// Save proxy URL
+proxyUrlInput.addEventListener("input", async function () {
+ const proxyUrl = proxyUrlInput.value;
+ await SettingsManager.saveProxyConfig(proxyUrl); // Save the proxy config
+});
+
+
+// ================ Command Options ================
+const use_shaka = document.getElementById("use-shaka");
+use_shaka.addEventListener("change", async function () {
+ await SettingsManager.saveUseShakaPackager(use_shaka.checked);
+});
+
+const use_ddownloader = document.getElementById("use-ddownloader");
+use_shaka.addEventListener("change", async function () {
+ await SettingsManager.saveUseShakaPackager(use_ddownloader.checked);
+});
+
+const downloader_name = document.getElementById("downloader-name");
+downloader_name.addEventListener("input", async function (event) {
+ console.log("input change", event);
+ await SettingsManager.saveExecutableName(downloader_name.value);
+});
+// =================================================
+
+// ================ Keys ================
+const clear = document.getElementById("clear");
+clear.addEventListener("click", async function () {
+ chrome.runtime.sendMessage({ type: "CLEAR" });
+ key_container.innerHTML = "";
+});
+
+async function createCommand(json, key_string) {
+ const metadata = JSON.parse(json);
+ const header_string = Object.entries(metadata.headers)
+ .map(([key, value]) => `-H "${key}: ${value.replace(/"/g, "'")}"`)
+ .join(" ");
+
+ // Assuming `metadata.url` is the URL to use, and output is derived from `metadata.id` or some other field
+ const output = metadata.id || "output"; // Change this according to how you want to generate the output name.
+
+ return `DDownloader -u "${metadata.url}" ${header_string} ${key_string} -o "${output}"`;
+}
+
+async function appendLog(result) {
+ const key_string = result.keys
+ .map((key) => `--key ${key.kid}:${key.k}`)
+ .join(" ");
+
+ const logContainer = document.createElement("div");
+ logContainer.classList.add("log-container");
+ logContainer.innerHTML = `
+
+ `;
+
+ const keysInput = logContainer.querySelector(".key-copy");
+ keysInput.addEventListener("click", () => {
+ navigator.clipboard.writeText(key_string);
+ });
+
+ if (result.manifests.length > 0) {
+ const command = logContainer.querySelector("#command");
+
+ const select = logContainer.querySelector("#manifest");
+ select.addEventListener("change", async () => {
+ command.value = await createCommand(select.value, key_string);
+ });
+ result.manifests.forEach((manifest) => {
+ const option = new Option(
+ `[${manifest.type}] ${manifest.url}`,
+ JSON.stringify(manifest)
+ );
+ select.add(option);
+ });
+ command.value = await createCommand(select.value, key_string);
+
+ const manifest_copy = logContainer.querySelector(".manifest-copy");
+ manifest_copy.addEventListener("click", () => {
+ navigator.clipboard.writeText(JSON.parse(select.value).url);
+ });
+
+ const command_copy = logContainer.querySelector(".command-copy");
+ command_copy.addEventListener("click", () => {
+ navigator.clipboard.writeText(command.value);
+ });
+ }
+
+ const toggleButtons = logContainer.querySelector(".toggleButton");
+ toggleButtons.addEventListener("click", function () {
+ const expandableDiv = this.nextElementSibling;
+ if (expandableDiv.classList.contains("collapsed")) {
+ toggleButtons.innerHTML = "-";
+ expandableDiv.classList.remove("collapsed");
+ expandableDiv.classList.add("expanded");
+ } else {
+ toggleButtons.innerHTML = "+";
+ expandableDiv.classList.remove("expanded");
+ expandableDiv.classList.add("collapsed");
+ }
+ });
+
+ key_container.appendChild(logContainer);
+}
+
+chrome.storage.onChanged.addListener(async (changes, areaName) => {
+ if (areaName === "local") {
+ for (const [key, values] of Object.entries(changes)) {
+ await appendLog(values.newValue);
+ }
+ }
+});
+
+function checkLogs() {
+ chrome.runtime.sendMessage({ type: "GET_LOGS" }, (response) => {
+ if (response) {
+ response.forEach(async (result) => {
+ await appendLog(result);
+ });
+ }
+ });
+}
+
+document.addEventListener("DOMContentLoaded", async function () {
+ enabled.checked = await SettingsManager.getEnabled();
+ use_shaka.checked = await SettingsManager.getUseShakaPackager();
+ downloader_name.value = await SettingsManager.getExecutableName();
+ await RemoteCDMManager.loadSetAllRemoteCDMs();
+ await RemoteCDMManager.selectRemoteCDM(
+ await RemoteCDMManager.getSelectedRemoteCDM()
+ );
+ checkLogs();
+});
+
+document.addEventListener("DOMContentLoaded", async function () {
+ const isProxyEnabled = await SettingsManager.getProxyEnabled();
+ enableProxy.checked = isProxyEnabled;
+ if (isProxyEnabled) {
+ proxyConfig.style.display = "block";
+ }
+
+ const savedProxyUrl = await SettingsManager.getProxy();
+ const savedProxyPort = await SettingsManager.getProxyPort();
+
+ // Combine the proxy URL and port (if available)
+ if (savedProxyUrl && savedProxyPort) {
+ proxyUrlInput.value = `${savedProxyUrl}:${savedProxyPort}`;
+ } else {
+ proxyUrlInput.value = savedProxyUrl || '';
+ }
+});
+
+// ======================================