api docs, generator
This commit is contained in:
387
static/js/api.js
387
static/js/api.js
@@ -1,75 +1,342 @@
|
||||
const baseUrl = window.location.origin;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('baseUrl').textContent = baseUrl;
|
||||
document.querySelectorAll('[id^="curlUrl"]').forEach(element => {
|
||||
element.textContent = baseUrl;
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const baseEl = document.getElementById('baseUrl');
|
||||
if (baseEl) baseEl.textContent = baseUrl;
|
||||
|
||||
document.querySelectorAll('[id^="curlUrl"]').forEach((el) => {
|
||||
el.textContent = baseUrl;
|
||||
});
|
||||
});
|
||||
|
||||
function toggleEndpoint(id) {
|
||||
const element = document.getElementById(id);
|
||||
const bsCollapse = new bootstrap.Collapse(element, {
|
||||
toggle: true
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
new bootstrap.Collapse(el, { toggle: true });
|
||||
}
|
||||
|
||||
function showResponse(id) {
|
||||
const div = document.getElementById(id);
|
||||
const body = document.getElementById(id + '-body');
|
||||
if (div) div.style.display = 'block';
|
||||
if (body) body.textContent = 'Loading...';
|
||||
return { div, body };
|
||||
}
|
||||
|
||||
function formatHeaders(headers) {
|
||||
const entries = [];
|
||||
for (const [k, v] of headers.entries()) entries.push([k, v]);
|
||||
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
return entries.map(([k, v]) => `${k}: ${v}`).join('\n');
|
||||
}
|
||||
|
||||
function cacheHeaderSummary(headers) {
|
||||
const keys = ['x-from-cache', 'x-cache-type', 'x-generated-at', 'content-type', 'content-disposition'];
|
||||
const out = [];
|
||||
for (const k of keys) {
|
||||
const v = headers.get(k);
|
||||
if (v !== null) out.push(`${k}: ${v}`);
|
||||
}
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
async function readBodyAuto(response) {
|
||||
const ct = (response.headers.get('content-type') || '').toLowerCase();
|
||||
const isJson = ct.includes('application/json');
|
||||
if (isJson) {
|
||||
try {
|
||||
return { kind: 'json', data: await response.json(), contentType: ct };
|
||||
} catch {
|
||||
return { kind: 'text', data: await response.text(), contentType: ct };
|
||||
}
|
||||
}
|
||||
return { kind: 'text', data: await response.text(), contentType: ct };
|
||||
}
|
||||
|
||||
function safeParseJsonFromTextarea(textareaId) {
|
||||
const el = document.getElementById(textareaId);
|
||||
if (!el) throw new Error(`Missing textarea: ${textareaId}`);
|
||||
const raw = el.value;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid JSON in textarea "${textareaId}": ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function guessExtensionFromContentType(ct) {
|
||||
const s = (ct || '').toLowerCase();
|
||||
if (s.includes('application/json')) return 'json';
|
||||
if (s.includes('text/csv')) return 'csv';
|
||||
if (s.includes('application/javascript')) return 'js';
|
||||
if (s.includes('text/plain')) return 'txt';
|
||||
return 'txt';
|
||||
}
|
||||
|
||||
function firstLines(text, maxLines = 80) {
|
||||
const lines = String(text || '').split(/\r?\n/);
|
||||
return lines.slice(0, maxLines).join('\n');
|
||||
}
|
||||
|
||||
function tryApi(endpoint, method = 'GET', body = null) {
|
||||
const responseId = 'response-' + endpoint.replace(/\//g, '-');
|
||||
const { body: out } = showResponse(responseId);
|
||||
|
||||
const url = baseUrl + '/api/' + endpoint;
|
||||
const opts = { method, headers: {} };
|
||||
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
opts.body = body == null ? '{}' : (typeof body === 'string' ? body : JSON.stringify(body));
|
||||
}
|
||||
|
||||
fetch(url, opts)
|
||||
.then(async (resp) => {
|
||||
const parsed = await readBodyAuto(resp);
|
||||
|
||||
const meta = [
|
||||
`HTTP ${resp.status} ${resp.statusText}`,
|
||||
cacheHeaderSummary(resp.headers),
|
||||
'\n--- Headers ---\n' + formatHeaders(resp.headers),
|
||||
'\n--- Body ---\n'
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
if (!resp.ok) {
|
||||
const msg = parsed.kind === 'json'
|
||||
? (parsed.data?.error || JSON.stringify(parsed.data, null, 2))
|
||||
: String(parsed.data || '');
|
||||
throw new Error(`${meta}${msg}`);
|
||||
}
|
||||
|
||||
const pretty = parsed.kind === 'json'
|
||||
? JSON.stringify(parsed.data, null, 2)
|
||||
: String(parsed.data ?? '');
|
||||
|
||||
out.textContent = meta + pretty;
|
||||
})
|
||||
.catch((err) => {
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
function tryEndpoint(endpoint, method = 'GET') {
|
||||
const url = baseUrl + '/api/' + endpoint;
|
||||
function tryPath(path, method = 'GET') {
|
||||
const normalized = String(path || '').trim();
|
||||
const key = normalized.replace(/[^\w-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
const responseId = 'response-' + (key || 'path');
|
||||
const { body: out } = showResponse(responseId);
|
||||
|
||||
const url = baseUrl + normalized;
|
||||
fetch(url, { method })
|
||||
.then(async (resp) => {
|
||||
const parsed = await readBodyAuto(resp);
|
||||
|
||||
const meta = [
|
||||
`HTTP ${resp.status} ${resp.statusText}`,
|
||||
cacheHeaderSummary(resp.headers),
|
||||
'\n--- Headers ---\n' + formatHeaders(resp.headers),
|
||||
'\n--- Body ---\n'
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
if (!resp.ok) {
|
||||
const msg = parsed.kind === 'json'
|
||||
? (parsed.data?.error || JSON.stringify(parsed.data, null, 2))
|
||||
: String(parsed.data || '');
|
||||
throw new Error(`${meta}${msg}`);
|
||||
}
|
||||
|
||||
const pretty = parsed.kind === 'json'
|
||||
? JSON.stringify(parsed.data, null, 2)
|
||||
: String(parsed.data ?? '');
|
||||
|
||||
out.textContent = meta + pretty;
|
||||
})
|
||||
.catch((err) => {
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
function tryApiJsonTextarea(endpoint, textareaId) {
|
||||
try {
|
||||
const body = safeParseJsonFromTextarea(textareaId);
|
||||
tryApi(endpoint, 'POST', body);
|
||||
} catch (e) {
|
||||
const responseId = 'response-' + endpoint.replace(/\//g, '-');
|
||||
const responseDiv = document.getElementById(responseId);
|
||||
const responseBody = document.getElementById(responseId + '-body');
|
||||
|
||||
responseDiv.style.display = 'block';
|
||||
responseBody.textContent = 'Loading...';
|
||||
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
responseBody.textContent = JSON.stringify(data, null, 2);
|
||||
})
|
||||
.catch(error => {
|
||||
responseBody.textContent = 'Error: ' + error.message;
|
||||
});
|
||||
const { body: out } = showResponse(responseId);
|
||||
out.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function tryInvalidateCountry() {
|
||||
const countryInput = document.getElementById('invalidateCountry');
|
||||
const country = countryInput.value.trim().toUpperCase();
|
||||
|
||||
if (!country || country.length !== 2) {
|
||||
alert('Please enter a valid 2-letter country code (e.g., CN, RU, US)');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = baseUrl + '/api/cache/invalidate/' + country;
|
||||
const responseDiv = document.getElementById('response-cache-invalidate');
|
||||
const responseBody = document.getElementById('response-cache-invalidate-body');
|
||||
|
||||
responseDiv.style.display = 'block';
|
||||
responseBody.textContent = 'Loading...';
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const countryInput = document.getElementById('invalidateCountry');
|
||||
const country = (countryInput?.value || '').trim().toUpperCase();
|
||||
|
||||
const { body: out } = showResponse('response-cache-invalidate');
|
||||
|
||||
if (!country || country.length !== 2) {
|
||||
out.textContent = 'Error: Please enter a valid 2-letter country code (e.g., CN, RU, US).';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(baseUrl + '/api/cache/invalidate/' + country, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}'
|
||||
})
|
||||
.then(async (resp) => {
|
||||
const parsed = await readBodyAuto(resp);
|
||||
if (!resp.ok) {
|
||||
const msg = parsed.kind === 'json'
|
||||
? (parsed.data?.error || JSON.stringify(parsed.data, null, 2))
|
||||
: String(parsed.data || '');
|
||||
throw new Error(msg || `HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
out.textContent = (parsed.kind === 'json')
|
||||
? JSON.stringify(parsed.data, null, 2)
|
||||
: String(parsed.data ?? '');
|
||||
|
||||
if (parsed.kind === 'json' && parsed.data?.success && countryInput) {
|
||||
countryInput.value = '';
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
responseBody.textContent = JSON.stringify(data, null, 2);
|
||||
if (data.success) {
|
||||
countryInput.value = '';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
responseBody.textContent = 'Error: ' + error.message;
|
||||
.catch((err) => {
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchAsBlob(url, method, jsonBody) {
|
||||
const resp = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(jsonBody ?? {})
|
||||
});
|
||||
|
||||
const headersText = formatHeaders(resp.headers);
|
||||
const cacheSummary = cacheHeaderSummary(resp.headers);
|
||||
const ct = resp.headers.get('content-type') || '';
|
||||
const cd = resp.headers.get('content-disposition') || '';
|
||||
|
||||
const blob = await resp.blob();
|
||||
|
||||
if (!resp.ok) {
|
||||
let errText = '';
|
||||
try { errText = await blob.text(); } catch { errText = ''; }
|
||||
throw new Error(
|
||||
`HTTP ${resp.status} ${resp.statusText}\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Body ---\n${errText}`.trim()
|
||||
);
|
||||
}
|
||||
|
||||
return { resp, blob, headersText, cacheSummary, contentType: ct, contentDisposition: cd };
|
||||
}
|
||||
|
||||
function makeDownloadLink(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.textContent = filename;
|
||||
a.className = 'link-primary api-download-link';
|
||||
a.onclick = () => setTimeout(() => URL.revokeObjectURL(url), 2500);
|
||||
return a;
|
||||
}
|
||||
|
||||
function downloadFromApiJsonTextarea(endpoint, textareaId, fileBaseName) {
|
||||
const responseId = 'response-' + endpoint.replace(/\//g, '-');
|
||||
const { body: out } = showResponse(responseId);
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = safeParseJsonFromTextarea(textareaId);
|
||||
} catch (e) {
|
||||
out.textContent = 'Error: ' + e.message;
|
||||
return;
|
||||
}
|
||||
|
||||
const url = baseUrl + '/api/' + endpoint;
|
||||
|
||||
fetchAsBlob(url, 'POST', body)
|
||||
.then(async ({ blob, headersText, cacheSummary, contentType, contentDisposition }) => {
|
||||
const ext = guessExtensionFromContentType(contentType);
|
||||
const filename = `${fileBaseName}.${ext}`;
|
||||
|
||||
let preview = '';
|
||||
try {
|
||||
const txt = await blob.text();
|
||||
preview = firstLines(txt, 80);
|
||||
} catch {
|
||||
preview = '(binary content)';
|
||||
}
|
||||
|
||||
out.textContent =
|
||||
`OK\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Preview (first lines) ---\n${preview}\n\n--- Download ---\n`;
|
||||
|
||||
const link = makeDownloadLink(blob, filename);
|
||||
out.parentElement.appendChild(link);
|
||||
})
|
||||
.catch((err) => {
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function previewTextFromGenerate(textareaId) {
|
||||
const { body: out } = showResponse('response-generate-download');
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = safeParseJsonFromTextarea(textareaId);
|
||||
} catch (e) {
|
||||
out.textContent = 'Error: ' + e.message;
|
||||
return;
|
||||
}
|
||||
|
||||
fetchAsBlob(baseUrl + '/api/generate', 'POST', body)
|
||||
.then(async ({ blob, headersText, cacheSummary, contentType }) => {
|
||||
let text = '';
|
||||
try { text = await blob.text(); } catch { text = '(binary content)'; }
|
||||
|
||||
out.textContent =
|
||||
`OK\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Preview (first ~80 lines) ---\n${firstLines(text, 80)}\n`;
|
||||
})
|
||||
.catch((err) => {
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFromGenerate(textareaId, fileBaseName) {
|
||||
const { body: out } = showResponse('response-generate-download');
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = safeParseJsonFromTextarea(textareaId);
|
||||
} catch (e) {
|
||||
out.textContent = 'Error: ' + e.message;
|
||||
return;
|
||||
}
|
||||
|
||||
fetchAsBlob(baseUrl + '/api/generate', 'POST', body)
|
||||
.then(async ({ blob, headersText, cacheSummary, contentType, contentDisposition }) => {
|
||||
const ext = guessExtensionFromContentType(contentType);
|
||||
let filename = `${fileBaseName}.${ext}`;
|
||||
|
||||
const m = /filename="?([^"]+)"?/i.exec(contentDisposition || '');
|
||||
if (m && m[1]) filename = m[1];
|
||||
|
||||
let preview = '';
|
||||
try {
|
||||
const txt = await blob.text();
|
||||
preview = firstLines(txt, 80);
|
||||
} catch {
|
||||
preview = '(binary content)';
|
||||
}
|
||||
|
||||
out.textContent =
|
||||
`OK\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Preview (first lines) ---\n${preview}\n\n--- Download ---\n`;
|
||||
|
||||
const link = makeDownloadLink(blob, filename);
|
||||
out.parentElement.appendChild(link);
|
||||
})
|
||||
.catch((err) => {
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
25
static/js/base.js
Normal file
25
static/js/base.js
Normal file
@@ -0,0 +1,25 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const dot = document.getElementById("apiStatusDot");
|
||||
const text = document.getElementById("apiStatusText");
|
||||
|
||||
if (!dot || !text) return;
|
||||
|
||||
fetch("/health", { method: "GET" })
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error("API not healthy");
|
||||
dot.classList.remove("bg-secondary");
|
||||
dot.classList.add("bg-success");
|
||||
text.textContent = "API Online";
|
||||
text.classList.remove("text-muted");
|
||||
text.classList.add("text-success");
|
||||
})
|
||||
.catch(() => {
|
||||
dot.classList.remove("bg-secondary");
|
||||
dot.classList.add("bg-danger");
|
||||
text.textContent = "API Offline";
|
||||
text.classList.remove("text-muted");
|
||||
text.classList.add("text-danger");
|
||||
});
|
||||
|
||||
});
|
||||
194
static/js/generator.js
Normal file
194
static/js/generator.js
Normal file
@@ -0,0 +1,194 @@
|
||||
const baseUrl = window.location.origin;
|
||||
|
||||
const variantsByApp = {
|
||||
haproxy: [
|
||||
{ value: "map", label: "map (recommended)" },
|
||||
{ value: "acl", label: "acl" },
|
||||
{ value: "lua", label: "lua" },
|
||||
],
|
||||
apache: [
|
||||
{ value: "24", label: "24 (recommended)" },
|
||||
{ value: "22", label: "22 (legacy)" },
|
||||
],
|
||||
nginx: [
|
||||
{ value: "geo", label: "geo (recommended)" },
|
||||
{ value: "deny", label: "deny (recommended)" },
|
||||
{ value: "map", label: "map (not recommended)" },
|
||||
],
|
||||
};
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function setBaseUrl() {
|
||||
const el = $("baseUrl");
|
||||
if (el) el.textContent = baseUrl;
|
||||
}
|
||||
|
||||
function normalizeCountries(input) {
|
||||
return String(input || "")
|
||||
.split(",")
|
||||
.map(s => s.trim().toUpperCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function updateModeUI() {
|
||||
const mode = $("pyMode").value;
|
||||
|
||||
const rawOn = mode === "raw";
|
||||
const genOn = mode === "generate";
|
||||
|
||||
$("pyRawFormatBox").style.display = rawOn ? "block" : "none";
|
||||
$("pyAsJsBox").style.display = rawOn ? "block" : "none";
|
||||
$("pyJsVarBox").style.display = rawOn ? "block" : "none";
|
||||
|
||||
$("pyAppTypeBox").style.display = genOn ? "block" : "none";
|
||||
$("pyAppVariantBox").style.display = genOn ? "block" : "none";
|
||||
|
||||
if (genOn) {
|
||||
updateVariantOptions();
|
||||
} else {
|
||||
updateRawJsFields();
|
||||
}
|
||||
}
|
||||
|
||||
function updateVariantOptions() {
|
||||
const app = $("pyAppType").value;
|
||||
const select = $("pyAppVariant");
|
||||
const hint = $("variantHint");
|
||||
|
||||
select.innerHTML = "";
|
||||
(variantsByApp[app] || []).forEach(v => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = v.value;
|
||||
opt.textContent = v.label;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
if (app === "haproxy") hint.textContent = "Recommended: haproxy + map";
|
||||
else if (app === "apache") hint.textContent = "Recommended: apache + 24";
|
||||
else if (app === "nginx") hint.textContent = "Recommended: nginx + geo or deny (avoid map)";
|
||||
else hint.textContent = "";
|
||||
}
|
||||
|
||||
function updateRawJsFields() {
|
||||
const fmt = $("pyRawFormat").value;
|
||||
const asJs = $("pyAsJs").value === "true";
|
||||
|
||||
const allowJs = fmt === "raw-cidr_json";
|
||||
$("pyAsJs").disabled = !allowJs;
|
||||
$("pyJsVar").disabled = !allowJs || !asJs;
|
||||
|
||||
if (!allowJs) {
|
||||
$("pyAsJs").value = "false";
|
||||
}
|
||||
}
|
||||
|
||||
function buildPythonScript() {
|
||||
const mode = $("pyMode").value;
|
||||
const countries = normalizeCountries($("pyCountries").value);
|
||||
const aggregate = $("pyAggregate").value === "true";
|
||||
const useCache = $("pyCache").value === "true";
|
||||
|
||||
let endpoint = "";
|
||||
const payload = { countries, aggregate, use_cache: useCache };
|
||||
|
||||
if (mode === "raw") {
|
||||
endpoint = "/api/generate/raw";
|
||||
payload.app_type = $("pyRawFormat").value;
|
||||
|
||||
if (payload.app_type === "raw-cidr_json") {
|
||||
const asJs = $("pyAsJs").value === "true";
|
||||
payload.as_js = asJs;
|
||||
if (asJs) payload.js_var = $("pyJsVar").value || "geoipBlocklist";
|
||||
}
|
||||
} else {
|
||||
endpoint = "/api/generate";
|
||||
payload.app_type = $("pyAppType").value;
|
||||
payload.app_variant = $("pyAppVariant").value;
|
||||
}
|
||||
|
||||
const script = `#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
|
||||
BASE_URL = ${JSON.stringify(baseUrl)}
|
||||
ENDPOINT = ${JSON.stringify(endpoint)}
|
||||
|
||||
payload = ${JSON.stringify(payload, null, 4)}
|
||||
|
||||
resp = requests.post(BASE_URL + ENDPOINT, json=payload, timeout=120)
|
||||
|
||||
print("Status:", resp.status_code)
|
||||
print("X-From-Cache:", resp.headers.get("X-From-Cache"))
|
||||
print("X-Cache-Type:", resp.headers.get("X-Cache-Type"))
|
||||
print("X-Generated-At:", resp.headers.get("X-Generated-At"))
|
||||
|
||||
ct = (resp.headers.get("Content-Type") or "").lower()
|
||||
|
||||
if resp.status_code >= 400:
|
||||
# try show JSON error, else text
|
||||
try:
|
||||
print(json.dumps(resp.json(), indent=2))
|
||||
except Exception:
|
||||
print(resp.text)
|
||||
raise SystemExit(1)
|
||||
|
||||
if "application/json" in ct:
|
||||
print(json.dumps(resp.json(), indent=2))
|
||||
else:
|
||||
filename = "output"
|
||||
cd = resp.headers.get("Content-Disposition") or ""
|
||||
m = re.search(r'filename="?([^"]+)"?', cd)
|
||||
if m:
|
||||
filename = m.group(1)
|
||||
else:
|
||||
# fallback extension
|
||||
if "text/csv" in ct:
|
||||
filename += ".csv"
|
||||
elif "javascript" in ct:
|
||||
filename += ".js"
|
||||
elif "text/plain" in ct:
|
||||
filename += ".txt"
|
||||
else:
|
||||
filename += ".bin"
|
||||
|
||||
with open(filename, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
print("Saved to:", filename)
|
||||
`;
|
||||
|
||||
$("pythonScriptOutput").textContent = script;
|
||||
}
|
||||
|
||||
async function copyPythonScript() {
|
||||
const text = $("pythonScriptOutput").textContent || "";
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
function bind() {
|
||||
const topCopy = document.getElementById("btnCopyPyTop");
|
||||
if (topCopy) topCopy.addEventListener("click", copyPythonScript);
|
||||
|
||||
$("pyMode").addEventListener("change", updateModeUI);
|
||||
|
||||
$("pyAppType").addEventListener("change", updateVariantOptions);
|
||||
$("pyRawFormat").addEventListener("change", updateRawJsFields);
|
||||
$("pyAsJs").addEventListener("change", updateRawJsFields);
|
||||
|
||||
$("btnGenPy").addEventListener("click", () => {
|
||||
updateRawJsFields();
|
||||
buildPythonScript();
|
||||
});
|
||||
|
||||
$("btnCopyPy").addEventListener("click", copyPythonScript);
|
||||
|
||||
updateModeUI();
|
||||
buildPythonScript();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
setBaseUrl();
|
||||
bind();
|
||||
});
|
||||
Reference in New Issue
Block a user