Working version
This commit is contained in:
40
popup/drawList.js
Normal file
40
popup/drawList.js
Normal file
@@ -0,0 +1,40 @@
|
||||
let psshs=chrome.extension.getBackgroundPage().psshs;
|
||||
let requests=chrome.extension.getBackgroundPage().requests;
|
||||
var userInputs={};
|
||||
|
||||
document.getElementById('psshButton').addEventListener("click", () => drawList(psshs, 'pssh'));
|
||||
document.getElementById('licenseButton').addEventListener("click", () => drawList(requests.map(r => r['url']), 'license'));
|
||||
|
||||
function writeListElement(items, outputVar, searchStr) {
|
||||
document.getElementById("items").innerHTML = '';
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (!searchStr || item.includes(searchStr)) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = item;
|
||||
li.addEventListener('click', () => itemSelected(index, item, outputVar));
|
||||
document.getElementById("items").appendChild(li);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawList(items, outputVar) {
|
||||
document.getElementById('home').style.display='none';
|
||||
document.getElementById('chooserContainer').style.display='grid';
|
||||
document.getElementById('toggleHistory').style.display='none';
|
||||
|
||||
writeListElement(items, outputVar, null)
|
||||
document.getElementById("chooserSearch").addEventListener('input', event => {
|
||||
const searchStr = event.target.value.toLowerCase();
|
||||
writeListElement(items, outputVar, searchStr)
|
||||
});
|
||||
}
|
||||
|
||||
function itemSelected(index, item, outputVar){
|
||||
userInputs[outputVar]=index;
|
||||
document.getElementById(outputVar).value=item;
|
||||
document.getElementById('chooserContainer').style.display='none';
|
||||
document.getElementById('home').style.display='grid';
|
||||
document.getElementById('toggleHistory').style.display='grid';
|
||||
document.getElementById("chooserSearch").value=""
|
||||
}
|
||||
15
popup/editScheme.js
Normal file
15
popup/editScheme.js
Normal file
@@ -0,0 +1,15 @@
|
||||
document.getElementById("editSchemeButton").addEventListener("click", () => {
|
||||
document.getElementById("editSchemeContainer").style.display = "grid"
|
||||
document.getElementById('home').style.display='none';
|
||||
document.getElementById('toggleHistory').style.display='none';
|
||||
})
|
||||
|
||||
document.getElementById("editSchemeOK").addEventListener("click", () => {
|
||||
document.getElementById("editSchemeContainer").style.display = "none"
|
||||
document.getElementById('home').style.display='grid';
|
||||
document.getElementById('toggleHistory').style.display='grid';
|
||||
})
|
||||
|
||||
document.getElementById("schemeSelect").addEventListener("input", async () => {
|
||||
document.getElementById("schemeCode").value = await fetch(`/python/schemes/${document.getElementById("schemeSelect").value}.py`).then(res=>res.text())
|
||||
})
|
||||
17
popup/history.html
Normal file
17
popup/history.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Widevine L3 Guessor - History</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="history">
|
||||
<a href="./main.html"><button>🔙 Back</button></a>
|
||||
<button id="saveHistory">💾 Save History</button>
|
||||
<button id="clearHistory">❌ Clear History</button>
|
||||
<div id="histDisp"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="/libs/jsonview.js"></script>
|
||||
<script src="./history.js"></script>
|
||||
</html>
|
||||
28
popup/history.js
Normal file
28
popup/history.js
Normal file
@@ -0,0 +1,28 @@
|
||||
let psshs=chrome.extension.getBackgroundPage().psshs;
|
||||
function showHistory(){
|
||||
chrome.storage.local.get(null, (data => {
|
||||
let tree=jsonview.renderJSON(JSON.stringify(data), document.getElementById('histDisp'));
|
||||
jsonview.toggleNode(tree);
|
||||
}));
|
||||
}
|
||||
|
||||
function saveHistory(){
|
||||
chrome.storage.local.get(null, (data => {
|
||||
let blob = new Blob([JSON.stringify(data, null, "\t")], {type: "text/plain"});
|
||||
let a = document.createElement('a');
|
||||
a.download = 'wvgHistory.json';
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.click();
|
||||
}));
|
||||
}
|
||||
|
||||
function clearHistory(){
|
||||
if(confirm("Do you really want to clear history?")){
|
||||
chrome.storage.local.clear();
|
||||
document.getElementById('histDisp').innerHTML="";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('saveHistory').addEventListener("click", saveHistory);
|
||||
document.getElementById('clearHistory').addEventListener("click", clearHistory);
|
||||
showHistory()
|
||||
312
popup/main.html
Normal file
312
popup/main.html
Normal file
@@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Widevine L3 Guessor 2024</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<script src="/libs/pyodide/pyodide.js"></script>
|
||||
<style>
|
||||
body { min-height: 100vh; }
|
||||
.section-card { margin-bottom: 1.5rem; }
|
||||
.main-container { max-width: 1200px; }
|
||||
.extraction-section { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); }
|
||||
.crawlflix-section { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); }
|
||||
.clearkey-section { background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 20%); }
|
||||
.form-label { font-weight: 600; }
|
||||
.card-header h5 { margin: 0; }
|
||||
.status-area { min-height: 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container main-container p-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="text-center">
|
||||
<h2 class="mb-3"><i class="fas fa-shield-alt text-primary"></i> Widevine L3 Guessor 2024</h2>
|
||||
<p class="text-muted mb-3">Extract Widevine keys and send directly to CrawlFlix for seamless content processing</p>
|
||||
<a href="./history.html" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-history"></i> Show History
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No EME Detection -->
|
||||
<div id="noEME" class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning text-center py-4">
|
||||
<h4><i class="fas fa-exclamation-triangle"></i> No Widevine Content Detected</h4>
|
||||
<p class="mb-0">Open a widevine-protected website and try again!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div id="home" class="hidden">
|
||||
<div class="row">
|
||||
<!-- Key Extraction Column -->
|
||||
<div class="col-lg-6 col-xl-5">
|
||||
<div class="card section-card extraction-section">
|
||||
<div class="card-header py-3">
|
||||
<h5><i class="fas fa-key text-success"></i> Key Extraction</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form id="wvForm">
|
||||
<div class="mb-3">
|
||||
<label for="pssh" class="form-label">PSSH</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="pssh" class="form-control font-monospace" disabled>
|
||||
<button type="button" id="psshButton" class="btn btn-outline-primary">
|
||||
<i class="fas fa-mouse-pointer"></i> Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="license" class="form-label">License URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="license" class="form-control font-monospace" disabled>
|
||||
<button type="button" id="licenseButton" class="btn btn-outline-primary">
|
||||
<i class="fas fa-mouse-pointer"></i> Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-8">
|
||||
<label for="schemeSelect" class="form-label">Challenge Scheme</label>
|
||||
<select id="schemeSelect" class="form-select">
|
||||
<option value="Amazon">Amazon</option>
|
||||
<option value="Allente">Allente</option>
|
||||
<option value="CanalPlusVOD">CanalPlus (VOD)</option>
|
||||
<option value="CanalPlusLive">CanalPlus (Live)</option>
|
||||
<option value="Comcast">Comcast Xfinity</option>
|
||||
<option value="CommonWV" selected>CommonWV</option>
|
||||
<option value="DRMToday">DRMToday</option>
|
||||
<option value="Fantop">Fantop</option>
|
||||
<option value="GlobalTV">GlobalTV</option>
|
||||
<option value="Heuristic">Heuristic</option>
|
||||
<option value="moTV">moTV</option>
|
||||
<option value="NosTV">NosTV</option>
|
||||
<option value="oqee">Oqee</option>
|
||||
<option value="PolSatBoxGo">PolSatBoxGo</option>
|
||||
<option value="RedBee">Red Bee Media</option>
|
||||
<option value="Sling">Sling</option>
|
||||
<option value="thePlatform">thePlatform</option>
|
||||
<option value="VdoCipher">VdoCipher</option>
|
||||
<option value="VUDRM">VUDRM</option>
|
||||
<option value="Vodafone">Vodafone</option>
|
||||
<option value="Youku">Youku</option>
|
||||
<option value="YouTube">YouTube</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4 d-flex align-items-end">
|
||||
<button type="button" id="editSchemeButton" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="guess" class="btn btn-primary btn-lg w-100 mb-4">
|
||||
<i class="fas fa-magic me-2"></i> Extract Widevine Keys
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<label for="result" class="form-label">Extracted Keys</label>
|
||||
<textarea id="result" class="form-control font-monospace" rows="8"
|
||||
placeholder="Extracted keys will appear here..."></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CrawlFlix Integration Column -->
|
||||
<div class="col-lg-6 col-xl-7">
|
||||
<div class="card section-card crawlflix-section">
|
||||
<div class="card-header py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-download text-info"></i> CrawlFlix Integration</h5>
|
||||
<span class="badge bg-info">Auto-Workflow</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="crawlFlixUrl" class="form-label">
|
||||
<i class="fas fa-server"></i> CrawlFlix Server
|
||||
</label>
|
||||
<input type="text" id="crawlFlixUrl" class="form-control"
|
||||
placeholder="http://localhost:3000" value="http://localhost:3000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="mpdSelect" class="form-label">
|
||||
<i class="fas fa-file-video"></i> Detected MPD Manifests
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<select id="mpdSelect" class="form-select">
|
||||
<option value="">-- Auto-detected MPDs --</option>
|
||||
</select>
|
||||
<button type="button" id="refreshMPDs" class="btn btn-success">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="mpdUrl" class="form-label">MPD URL (Manual Entry)</label>
|
||||
<input type="text" id="mpdUrl" class="form-control font-monospace"
|
||||
placeholder="Or paste MPD URL manually...">
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<button type="button" id="sendToCrawlFlix" class="btn btn-primary btn-lg w-100 mb-2">
|
||||
<i class="fas fa-paper-plane me-2"></i> Send to CrawlFlix
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button type="button" id="copyKeys" class="btn btn-outline-secondary btn-lg w-100 mb-2">
|
||||
<i class="fas fa-copy me-2"></i> Copy Keys Only
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="crawlFlixStatus" class="status-area"></div>
|
||||
|
||||
<!-- Workflow Steps -->
|
||||
<div class="mt-4 p-3 bg-light rounded">
|
||||
<h6 class="text-muted mb-3"><i class="fas fa-list-ol"></i> Automated Workflow</h6>
|
||||
<div class="row text-center">
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-shield-alt fa-2x text-primary mb-2"></i>
|
||||
<p class="small mb-0">Extract Keys</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-search fa-2x text-success mb-2"></i>
|
||||
<p class="small mb-0">Detect MPD</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-paper-plane fa-2x text-info mb-2"></i>
|
||||
<p class="small mb-0">Send Data</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="workflow-step">
|
||||
<i class="fas fa-download fa-2x text-warning mb-2"></i>
|
||||
<p class="small mb-0">Start Download</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ClearKey Section -->
|
||||
<div id="ckHome" class="hidden">
|
||||
<div class="card section-card">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="mb-0"><i class="fas fa-unlock text-warning"></i> ClearKey Detected</h6>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<label for="ckResult" class="form-label">ClearKey Result:</label>
|
||||
<textarea id="ckResult" class="form-control font-monospace" rows="6" placeholder="ClearKey data will appear here..."></textarea>
|
||||
|
||||
<!-- CrawlFlix for ClearKey -->
|
||||
<div class="mt-3 pt-3 border-top">
|
||||
<h6><i class="fas fa-download text-info"></i> Send to CrawlFlix</h6>
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-12">
|
||||
<label for="crawlFlixUrlCK" class="form-label">CrawlFlix Server</label>
|
||||
<input type="text" id="crawlFlixUrlCK" class="form-control form-control-sm"
|
||||
placeholder="http://localhost:3000" value="http://localhost:3000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-10">
|
||||
<label for="mpdSelectCK" class="form-label">MPD Manifest</label>
|
||||
<select id="mpdSelectCK" class="form-select form-select-sm">
|
||||
<option value="">-- Detected MPDs --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-end">
|
||||
<button type="button" id="refreshMPDsCK" class="btn btn-success btn-sm w-100">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<input type="text" id="mpdUrlCK" class="form-control form-control-sm"
|
||||
placeholder="Or paste MPD URL manually...">
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button type="button" id="sendToCrawlFlixCK" class="btn btn-primary btn-sm flex-fill">
|
||||
<i class="fas fa-paper-plane"></i> Send to CrawlFlix
|
||||
</button>
|
||||
<button type="button" id="copyKeysCK" class="btn btn-outline-secondary btn-sm flex-fill">
|
||||
<i class="fas fa-copy"></i> Copy Keys
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="crawlFlixStatusCK" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden containers -->
|
||||
<div id="chooserContainer" class="hidden">
|
||||
<div class="card">
|
||||
<div class="card-body p-2">
|
||||
<input type="text" id="chooserSearch" class="form-control form-control-sm mb-2" placeholder="Search">
|
||||
<ul id="items" class="list-group list-group-flush"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editSchemeContainer" class="hidden">
|
||||
<div class="card">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="mb-0">Edit Scheme</h6>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<textarea id="schemeCode" class="form-control font-monospace" rows="8"></textarea>
|
||||
<button type="button" id="editSchemeOK" class="btn btn-primary btn-sm mt-2">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="updateNotice" class="alert alert-info alert-sm hidden">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Version =VER= update available!
|
||||
<a href="https://github.com/FoxRefire/wvg/archive/=HASH=.zip" class="alert-link">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./main.js" type="module"></script>
|
||||
<script src="./drawList.js"></script>
|
||||
<script src="./editScheme.js"></script>
|
||||
<script src="./updateNotice.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
701
popup/main.js
Normal file
701
popup/main.js
Normal file
@@ -0,0 +1,701 @@
|
||||
// === GLOBAL VARIABLES ===
|
||||
let psshs = chrome.extension.getBackgroundPage().psshs;
|
||||
let requests = chrome.extension.getBackgroundPage().requests;
|
||||
let pageURL = chrome.extension.getBackgroundPage().pageURL;
|
||||
let targetIds = chrome.extension.getBackgroundPage().targetIds;
|
||||
let clearkey = chrome.extension.getBackgroundPage().clearkey;
|
||||
let userInputs = {};
|
||||
|
||||
// === WIDEVINE KEY EXTRACTION ===
|
||||
class WidevineExtractor {
|
||||
static async extractKeys() {
|
||||
const guessButton = document.getElementById("guess");
|
||||
const resultTextarea = document.getElementById('result');
|
||||
|
||||
try {
|
||||
// UI feedback
|
||||
UIHelpers.setLoadingState(guessButton, true);
|
||||
document.body.style.cursor = "wait";
|
||||
|
||||
// Initialize Pyodide
|
||||
const pyodide = await loadPyodide();
|
||||
await pyodide.loadPackage([
|
||||
"certifi-2024.2.2-py3-none-any.whl",
|
||||
"charset_normalizer-3.3.2-py3-none-any.whl",
|
||||
"construct-2.8.8-py2.py3-none-any.whl",
|
||||
"idna-3.6-py3-none-any.whl",
|
||||
"packaging-23.2-py3-none-any.whl",
|
||||
"protobuf-4.24.4-cp312-cp312-emscripten_3_1_52_wasm32.whl",
|
||||
"pycryptodome-3.20.0-cp35-abi3-emscripten_3_1_52_wasm32.whl",
|
||||
"pymp4-1.4.0-py3-none-any.whl",
|
||||
"pyodide_http-0.2.1-py3-none-any.whl",
|
||||
"pywidevine-1.8.0-py3-none-any.whl",
|
||||
"requests-2.31.0-py3-none-any.whl",
|
||||
"urllib3-2.2.1-py3-none-any.whl"
|
||||
].map(e => "/libs/wheels/" + e));
|
||||
|
||||
// Configure Guesser
|
||||
pyodide.globals.set("pssh", document.getElementById('pssh').value);
|
||||
pyodide.globals.set("licUrl", requests[userInputs['license']]['url']);
|
||||
pyodide.globals.set("licHeaders", requests[userInputs['license']]['headers']);
|
||||
pyodide.globals.set("licBody", requests[userInputs['license']]['body']);
|
||||
|
||||
// Load Python scripts
|
||||
const [pre, after, scheme] = await Promise.all([
|
||||
fetch('/python/pre.py').then(res => res.text()),
|
||||
fetch('/python/after.py').then(res => res.text()),
|
||||
Promise.resolve(document.getElementById("schemeCode").value)
|
||||
]);
|
||||
|
||||
// Execute Python script
|
||||
const result = await pyodide.runPythonAsync([pre, scheme, after].join("\n"));
|
||||
resultTextarea.value = result;
|
||||
|
||||
// Save to history
|
||||
this.saveToHistory(result);
|
||||
|
||||
// Auto-update CrawlFlix integration
|
||||
CrawlFlixIntegration.updateAfterKeyExtraction();
|
||||
|
||||
StatusManager.show('Keys extracted successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Key extraction failed:', error);
|
||||
StatusManager.show(`Key extraction failed: ${error.message}`, 'error');
|
||||
} finally {
|
||||
// Reset UI
|
||||
UIHelpers.setLoadingState(guessButton, false);
|
||||
document.body.style.cursor = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
static saveToHistory(result) {
|
||||
const historyData = {
|
||||
PSSH: document.getElementById('pssh').value,
|
||||
KEYS: result.split("\n").slice(0, -1)
|
||||
};
|
||||
chrome.storage.local.set({[pageURL]: historyData}, null);
|
||||
}
|
||||
|
||||
static async autoSelect() {
|
||||
userInputs["license"] = 0;
|
||||
document.getElementById("license").value = requests[0]['url'];
|
||||
document.getElementById('pssh').value = psshs[0];
|
||||
|
||||
try {
|
||||
const selectRules = await fetch("/selectRules.conf").then(r => r.text());
|
||||
const rules = selectRules
|
||||
.replace(/\n^\s*$|\s*\/\/.*|\s*$/gm, "")
|
||||
.split("\n")
|
||||
.map(row => row.split("$$"));
|
||||
|
||||
for (const item of rules) {
|
||||
const search = requests.map(r => r['url']).findIndex(e => e.includes(item[0]));
|
||||
if (search >= 0) {
|
||||
if (item[1]) document.getElementById("schemeSelect").value = item[1];
|
||||
userInputs["license"] = search;
|
||||
document.getElementById("license").value = requests[search]['url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("schemeSelect").dispatchEvent(new Event("input"));
|
||||
} catch (error) {
|
||||
console.error('Auto-select failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === SMART MPD SELECTOR ===
|
||||
class SmartMPDSelector {
|
||||
static scoreAndRankMPDs(mpdUrls) {
|
||||
const scoredMPDs = mpdUrls.map(url => ({
|
||||
url,
|
||||
score: this.calculateMPDScore(url),
|
||||
reason: this.getSelectionReason(url)
|
||||
}));
|
||||
|
||||
// Trier par score décroissant
|
||||
return scoredMPDs.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
static calculateMPDScore(url) {
|
||||
let score = 0;
|
||||
const urlLower = url.toLowerCase();
|
||||
|
||||
// === CRITÈRES NÉGATIFS (à éviter) ===
|
||||
|
||||
// Éliminer les URLs non-MPD
|
||||
if (urlLower.includes('github.com') || urlLower.includes('manifest.json')) {
|
||||
return -1000; // Score très négatif
|
||||
}
|
||||
|
||||
// Éviter les URLs avec trop de redirections/proxies
|
||||
if (urlLower.includes('routemeup') || urlLower.includes('route=')) {
|
||||
score -= 200;
|
||||
}
|
||||
|
||||
// Éviter les URLs avec double-encoding ou trop complexes
|
||||
if (url.includes('__token__') && url.includes('%')) {
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
// === CRITÈRES POSITIFS (à privilégier) ===
|
||||
|
||||
// Privilégier les URLs directes CDN
|
||||
if (urlLower.includes('cdn.net') || urlLower.includes('vod-')) {
|
||||
score += 300;
|
||||
}
|
||||
|
||||
// Privilégier les URLs avec .mpd explicite
|
||||
if (url.endsWith('.mpd') || urlLower.includes('.mpd?')) {
|
||||
score += 200;
|
||||
}
|
||||
|
||||
// Privilégier les URLs courtes (moins de proxies)
|
||||
if (url.length < 300) {
|
||||
score += 100;
|
||||
} else if (url.length > 500) {
|
||||
score -= 50;
|
||||
}
|
||||
|
||||
// Privilégier les tokens JWT simples vs double tokens
|
||||
const tokenCount = (url.match(/token=/g) || []).length;
|
||||
if (tokenCount === 1) {
|
||||
score += 50;
|
||||
} else if (tokenCount > 1) {
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
// Privilégier les URLs sans encoding excessif
|
||||
const encodingScore = url.length - decodeURIComponent(url).length;
|
||||
if (encodingScore < 50) {
|
||||
score += 50;
|
||||
} else {
|
||||
score -= encodingScore;
|
||||
}
|
||||
|
||||
// === CRITÈRES SPÉCIFIQUES CANAL+ ===
|
||||
|
||||
// Détecter le pattern Canal+/Paramount+
|
||||
if (urlLower.includes('paramountplus') || urlLower.includes('viacom')) {
|
||||
score += 100;
|
||||
|
||||
// Privilégier les edge servers directs
|
||||
if (urlLower.includes('p-cdnvod-edge') && !urlLower.includes('routemeup')) {
|
||||
score += 150;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static getSelectionReason(url) {
|
||||
const urlLower = url.toLowerCase();
|
||||
|
||||
if (urlLower.includes('github.com')) {
|
||||
return 'Excluded: Not a media manifest';
|
||||
}
|
||||
|
||||
if (urlLower.includes('routemeup')) {
|
||||
return 'Router/proxy URL (lower priority)';
|
||||
}
|
||||
|
||||
if (urlLower.includes('p-cdnvod-edge') && !urlLower.includes('routemeup')) {
|
||||
return 'Direct CDN edge server (optimal)';
|
||||
}
|
||||
|
||||
if (url.includes('__token__')) {
|
||||
return 'Tokenized CDN URL';
|
||||
}
|
||||
|
||||
return 'Standard MPD URL';
|
||||
}
|
||||
|
||||
static getBestMPD(mpdUrls) {
|
||||
if (!mpdUrls || mpdUrls.length === 0) return null;
|
||||
if (mpdUrls.length === 1) return mpdUrls[0];
|
||||
|
||||
const rankedMPDs = this.scoreAndRankMPDs(mpdUrls);
|
||||
const best = rankedMPDs[0];
|
||||
|
||||
console.log('MPD Selection Results:', rankedMPDs);
|
||||
|
||||
// Ne retourner que si le score est positif
|
||||
return best.score > 0 ? best.url : null;
|
||||
}
|
||||
|
||||
static analyzeAndDisplayMPDs(mpdUrls) {
|
||||
const rankedMPDs = this.scoreAndRankMPDs(mpdUrls);
|
||||
|
||||
// Afficher l'analyse dans la console pour debug
|
||||
console.table(rankedMPDs.map(mpd => ({
|
||||
URL: this.truncateUrl(mpd.url),
|
||||
Score: mpd.score,
|
||||
Reason: mpd.reason
|
||||
})));
|
||||
|
||||
return rankedMPDs;
|
||||
}
|
||||
|
||||
static truncateUrl(url, maxLength = 60) {
|
||||
if (url.length <= maxLength) return url;
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const domain = urlObj.hostname;
|
||||
const filename = urlObj.pathname.split('/').pop();
|
||||
return `${domain}/.../${filename}`;
|
||||
} catch {
|
||||
return url.substring(0, maxLength) + '...';
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour mettre à jour l'UI avec le meilleur MPD
|
||||
static updateUIWithBestMPD(mpdUrls) {
|
||||
const bestMPD = this.getBestMPD(mpdUrls);
|
||||
|
||||
if (bestMPD) {
|
||||
// Auto-sélectionner le meilleur MPD
|
||||
const selects = ['mpdSelect', 'mpdSelectCK'];
|
||||
const inputs = ['mpdUrl', 'mpdUrlCK'];
|
||||
|
||||
selects.forEach((selectId, index) => {
|
||||
const select = document.getElementById(selectId);
|
||||
const input = document.getElementById(inputs[index]);
|
||||
|
||||
if (select && input) {
|
||||
select.value = bestMPD;
|
||||
input.value = bestMPD;
|
||||
|
||||
// Ajouter une classe pour indiquer la sélection auto
|
||||
select.classList.add('border-success');
|
||||
setTimeout(() => select.classList.remove('border-success'), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
const analysis = this.analyzeAndDisplayMPDs(mpdUrls);
|
||||
const bestAnalysis = analysis[0];
|
||||
|
||||
StatusManager.show(
|
||||
`Auto-selected best MPD: ${bestAnalysis.reason}`,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MPDDetector {
|
||||
static detectMPDUrls() {
|
||||
const backgroundPage = chrome.extension.getBackgroundPage();
|
||||
const detectedMPDs = backgroundPage.detectedMPDs || [];
|
||||
|
||||
console.log('Raw detected MPDs:', detectedMPDs);
|
||||
|
||||
return detectedMPDs
|
||||
.filter((url, index, arr) => arr.indexOf(url) === index) // Remove duplicates
|
||||
.filter(url => url && url.length > 0) // Remove empty URLs
|
||||
.filter(url => !url.includes('/chunk-') && !url.includes('/segment-')); // Filter segments
|
||||
}
|
||||
|
||||
static updateBadgeFromPopup() {
|
||||
const backgroundPage = chrome.extension.getBackgroundPage();
|
||||
if (backgroundPage && backgroundPage.updateExtensionBadge) {
|
||||
backgroundPage.updateExtensionBadge();
|
||||
}
|
||||
}
|
||||
|
||||
static updateDropdowns() {
|
||||
const mpdUrls = this.detectMPDUrls();
|
||||
const selects = ['mpdSelect', 'mpdSelectCK'];
|
||||
|
||||
selects.forEach(selectId => {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = '<option value="">-- Auto-detected MPDs --</option>';
|
||||
|
||||
// Utiliser le smart selector pour trier les MPDs
|
||||
const rankedMPDs = SmartMPDSelector.scoreAndRankMPDs(mpdUrls);
|
||||
|
||||
rankedMPDs.forEach((mpdInfo, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = mpdInfo.url;
|
||||
|
||||
// Ajouter des indicateurs visuels
|
||||
const prefix = index === 0 ? '⭐ [BEST] ' :
|
||||
mpdInfo.score > 0 ? '✓ ' : '⚠ ';
|
||||
|
||||
option.textContent = prefix + this.formatUrlForDisplay(mpdInfo.url);
|
||||
|
||||
// Ajouter des classes CSS pour styling
|
||||
if (index === 0) {
|
||||
option.style.fontWeight = 'bold';
|
||||
option.style.color = '#28a745';
|
||||
} else if (mpdInfo.score <= 0) {
|
||||
option.style.color = '#dc3545';
|
||||
}
|
||||
|
||||
select.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-sélection intelligente
|
||||
if (mpdUrls.length > 0) {
|
||||
SmartMPDSelector.updateUIWithBestMPD(mpdUrls);
|
||||
}
|
||||
|
||||
// Mettre à jour le status avec l'analyse
|
||||
StatusManager.show(
|
||||
`Found ${mpdUrls.length} MPD(s) - Auto-selected best candidate`,
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
static formatUrlForDisplay(url) {
|
||||
return SmartMPDSelector.truncateUrl(url, 80);
|
||||
}
|
||||
}
|
||||
|
||||
// === CRAWLFLIX INTEGRATION ===
|
||||
class CrawlFlixIntegration {
|
||||
static async sendToCrawlFlix(isClearchey = false) {
|
||||
const suffix = isClearchey ? 'CK' : '';
|
||||
const crawlFlixUrl = document.getElementById(`crawlFlixUrl${suffix}`).value || 'http://localhost:3000';
|
||||
const mpdUrl = document.getElementById(`mpdUrl${suffix}`).value;
|
||||
const resultTextarea = document.getElementById(isClearchey ? 'ckResult' : 'result');
|
||||
|
||||
// Validation
|
||||
if (!mpdUrl.trim()) {
|
||||
StatusManager.show('Please enter or select an MPD URL', 'error', suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resultTextarea.value.trim()) {
|
||||
StatusManager.show('No keys available to send', 'error', suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
StatusManager.show('Processing MPD and sending to CrawlFlix...', 'info', suffix);
|
||||
|
||||
// Parse and validate keys
|
||||
const keys = this.parseKeys(resultTextarea.value);
|
||||
if (keys.length === 0) {
|
||||
StatusManager.show('No valid keys found in the format key:value', 'error', suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test CrawlFlix connection and process MPD
|
||||
const mpdData = await this.processMPD(crawlFlixUrl, mpdUrl);
|
||||
|
||||
// Create success message
|
||||
const message = `✓ Successfully sent to CrawlFlix!
|
||||
${keys.length} key(s) • ${mpdData.videoTracks.length} video track(s) • ${mpdData.audioTracks.length} audio track(s) • ${mpdData.subtitles.length} subtitle(s)`;
|
||||
|
||||
StatusManager.show(message, 'success', suffix);
|
||||
|
||||
// Open CrawlFlix with pre-filled data
|
||||
this.openCrawlFlixTab(crawlFlixUrl, mpdUrl, resultTextarea.value);
|
||||
|
||||
} catch (error) {
|
||||
console.error('CrawlFlix send error:', error);
|
||||
StatusManager.show(`Failed to send: ${error.message}`, 'error', suffix);
|
||||
}
|
||||
}
|
||||
|
||||
static parseKeys(keysText) {
|
||||
return keysText
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && line.includes(':'))
|
||||
.map(line => {
|
||||
const [key, value] = line.split(':');
|
||||
return {
|
||||
key: key?.trim(),
|
||||
value: value?.trim()
|
||||
};
|
||||
})
|
||||
.filter(k => k.key && k.value && k.key.length > 0 && k.value.length > 0);
|
||||
}
|
||||
|
||||
static async processMPD(crawlFlixUrl, mpdUrl) {
|
||||
const response = await fetch(`${crawlFlixUrl}/processMPD`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mpdUrl })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`MPD processing failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static openCrawlFlixTab(crawlFlixUrl, mpdUrl, keysText) {
|
||||
const params = new URLSearchParams({
|
||||
mpdUrl: mpdUrl,
|
||||
keys: keysText,
|
||||
source: 'widevine-plugin'
|
||||
});
|
||||
|
||||
const crawlFlixTab = `${crawlFlixUrl}?${params.toString()}`;
|
||||
chrome.tabs.create({ url: crawlFlixTab });
|
||||
}
|
||||
|
||||
static copyKeys(resultTextareaId) {
|
||||
const textarea = document.getElementById(resultTextareaId);
|
||||
if (!textarea || !textarea.value.trim()) {
|
||||
StatusManager.show('No keys to copy', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(textarea.value).then(() => {
|
||||
StatusManager.show('Keys copied to clipboard!', 'success');
|
||||
}).catch(err => {
|
||||
console.error('Copy failed:', err);
|
||||
StatusManager.show('Failed to copy keys', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
static updateAfterKeyExtraction() {
|
||||
// Auto-refresh MPD detection after key extraction
|
||||
setTimeout(() => {
|
||||
MPDDetector.updateDropdowns();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
static loadSavedSettings() {
|
||||
chrome.storage.local.get(['crawlFlixUrl'], (result) => {
|
||||
if (result.crawlFlixUrl) {
|
||||
const inputs = ['crawlFlixUrl', 'crawlFlixUrlCK'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
if (input && !input.value) {
|
||||
input.value = result.crawlFlixUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static saveSettings() {
|
||||
const inputs = ['crawlFlixUrl', 'crawlFlixUrlCK'];
|
||||
inputs.forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.addEventListener('change', (e) => {
|
||||
chrome.storage.local.set({ crawlFlixUrl: e.target.value });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === STATUS MANAGER ===
|
||||
class StatusManager {
|
||||
static show(message, type, suffix = '') {
|
||||
const statusDiv = document.getElementById(`crawlFlixStatus${suffix}`);
|
||||
if (!statusDiv) return;
|
||||
|
||||
// Clear any existing content
|
||||
statusDiv.innerHTML = '';
|
||||
|
||||
// Create alert element
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${this.getBootstrapClass(type)} alert-dismissible fade show`;
|
||||
alert.innerHTML = `
|
||||
<i class="fas ${this.getIcon(type)} me-2"></i>
|
||||
${message.replace(/\n/g, '<br>')}
|
||||
<button type="button" class="btn-close btn-close-sm" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
statusDiv.appendChild(alert);
|
||||
|
||||
// Auto-dismiss after delay
|
||||
if (type === 'success' || type === 'info') {
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.classList.remove('show');
|
||||
setTimeout(() => alert.remove(), 300);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
static getBootstrapClass(type) {
|
||||
const mapping = {
|
||||
success: 'success',
|
||||
error: 'danger',
|
||||
info: 'info',
|
||||
warning: 'warning'
|
||||
};
|
||||
return mapping[type] || 'secondary';
|
||||
}
|
||||
|
||||
static getIcon(type) {
|
||||
const mapping = {
|
||||
success: 'fa-check-circle',
|
||||
error: 'fa-exclamation-triangle',
|
||||
info: 'fa-info-circle',
|
||||
warning: 'fa-exclamation-triangle'
|
||||
};
|
||||
return mapping[type] || 'fa-info';
|
||||
}
|
||||
}
|
||||
|
||||
// === UI HELPERS ===
|
||||
class UIHelpers {
|
||||
static setLoadingState(button, isLoading) {
|
||||
if (!button) return;
|
||||
|
||||
if (isLoading) {
|
||||
button.disabled = true;
|
||||
const originalText = button.innerHTML;
|
||||
button.dataset.originalText = originalText;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Extracting Keys...';
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.innerHTML = button.dataset.originalText || button.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
static copyToClipboard(text) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
// === EVENT LISTENERS ===
|
||||
class EventManager {
|
||||
static init() {
|
||||
this.setupWidevineListeners();
|
||||
this.setupCrawlFlixListeners();
|
||||
this.setupUIListeners();
|
||||
}
|
||||
|
||||
static setupWidevineListeners() {
|
||||
const guessButton = document.getElementById('guess');
|
||||
const resultTextarea = document.getElementById('result');
|
||||
|
||||
if (guessButton) {
|
||||
guessButton.addEventListener('click', () => WidevineExtractor.extractKeys());
|
||||
}
|
||||
|
||||
if (resultTextarea) {
|
||||
resultTextarea.addEventListener('click', function() {
|
||||
this.select();
|
||||
navigator.clipboard.writeText(this.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static setupCrawlFlixListeners() {
|
||||
// Dropdown selections
|
||||
['mpdSelect', 'mpdSelectCK'].forEach(selectId => {
|
||||
const select = document.getElementById(selectId);
|
||||
const urlInput = document.getElementById(selectId.replace('Select', 'Url'));
|
||||
if (select && urlInput) {
|
||||
select.addEventListener('change', (e) => {
|
||||
if (e.target.value) {
|
||||
urlInput.value = e.target.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh buttons
|
||||
['refreshMPDs', 'refreshMPDsCK'].forEach(buttonId => {
|
||||
const button = document.getElementById(buttonId);
|
||||
if (button) {
|
||||
button.addEventListener('click', () => MPDDetector.updateDropdowns());
|
||||
}
|
||||
});
|
||||
|
||||
// Send buttons
|
||||
const sendButton = document.getElementById('sendToCrawlFlix');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', () => CrawlFlixIntegration.sendToCrawlFlix(false));
|
||||
}
|
||||
|
||||
const sendButtonCK = document.getElementById('sendToCrawlFlixCK');
|
||||
if (sendButtonCK) {
|
||||
sendButtonCK.addEventListener('click', () => CrawlFlixIntegration.sendToCrawlFlix(true));
|
||||
}
|
||||
|
||||
// Copy buttons
|
||||
const copyButton = document.getElementById('copyKeys');
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', () => CrawlFlixIntegration.copyKeys('result'));
|
||||
}
|
||||
|
||||
const copyButtonCK = document.getElementById('copyKeysCK');
|
||||
if (copyButtonCK) {
|
||||
copyButtonCK.addEventListener('click', () => CrawlFlixIntegration.copyKeys('ckResult'));
|
||||
}
|
||||
}
|
||||
|
||||
static setupUIListeners() {
|
||||
// ClearKey result click handler
|
||||
const ckResult = document.getElementById('ckResult');
|
||||
if (ckResult) {
|
||||
ckResult.addEventListener('click', function() {
|
||||
this.select();
|
||||
navigator.clipboard.writeText(this.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === CORS FETCH HELPER ===
|
||||
window.corsFetch = (u, m, h, b) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(targetIds[0], {
|
||||
type: "FETCH",
|
||||
u: u,
|
||||
m: m,
|
||||
h: h,
|
||||
b: b
|
||||
}, {frameId: targetIds[1]}, res => {
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// === INITIALIZATION ===
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
EventManager.init();
|
||||
CrawlFlixIntegration.loadSavedSettings();
|
||||
CrawlFlixIntegration.saveSettings();
|
||||
});
|
||||
|
||||
// Main initialization logic
|
||||
if (clearkey) {
|
||||
// ClearKey detected
|
||||
document.getElementById('noEME').style.display = 'none';
|
||||
document.getElementById('ckHome').style.display = 'block';
|
||||
document.getElementById('ckResult').value = clearkey;
|
||||
|
||||
MPDDetector.updateDropdowns();
|
||||
StatusManager.show('ClearKey content detected', 'success');
|
||||
MPDDetector.updateBadgeFromPopup();
|
||||
} else if (psshs.length) {
|
||||
// Widevine detected
|
||||
document.getElementById('noEME').style.display = 'none';
|
||||
document.getElementById('home').style.display = 'block';
|
||||
|
||||
WidevineExtractor.autoSelect();
|
||||
MPDDetector.updateDropdowns();
|
||||
StatusManager.show('Widevine content detected', 'success');
|
||||
MPDDetector.updateBadgeFromPopup();
|
||||
// Auto-refresh MPD detection periodically
|
||||
setInterval(() => {
|
||||
MPDDetector.updateDropdowns();
|
||||
}, 3000);
|
||||
}
|
||||
99
popup/style.css
Normal file
99
popup/style.css
Normal file
@@ -0,0 +1,99 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0; /* Reset default margin */
|
||||
padding: 0; /* Reset default padding */
|
||||
}
|
||||
|
||||
body {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background: linear-gradient(45deg, #0d364c, #062535, #02141e);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
#noEME {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#updateNotice {
|
||||
justify-self: center;
|
||||
align-self: end;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#updateNotice a{
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
#wvForm {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#wvForm label {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#pssh, #license, #schemeSelect {
|
||||
width: 80%;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#psshButton, #licenseButton, #editSchemeButton {
|
||||
width: 20%;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#toggleHistory {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
#toggleHistory button {
|
||||
width: 20%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#guess {
|
||||
width: 20%;
|
||||
justify-self: center;
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
#result {
|
||||
width: 90%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
resize: none;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#chooserContainer {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#ckHome h3, label {
|
||||
color: white;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#ckHome label {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
#ckResult {
|
||||
width: 90%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
resize: none;
|
||||
justify-self: center;
|
||||
}
|
||||
10
popup/updateNotice.js
Normal file
10
popup/updateNotice.js
Normal file
@@ -0,0 +1,10 @@
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
cManifest=await fetch("/manifest.json").then(r=>r.json());
|
||||
rManifest=await fetch("https://raw.githubusercontent.com/FoxRefire/wvg/next/manifest.json").then(r=>r.json());
|
||||
if(cManifest.version < rManifest.version){
|
||||
let notice = document.getElementById("updateNotice");
|
||||
notice.style.display='block';
|
||||
notice.innerHTML = notice.innerHTML.replace("=VER=", rManifest.version);
|
||||
notice.innerHTML = notice.innerHTML.replace("=HASH=", rManifest.version_name);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user