FuelBoard Copy Editor: web editor + Save & Build for Localizable.strings
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>FuelBoard Copy Editor</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 16px; background: #111418; color: #e6e8eb;
|
||||
font: 15px/1.45 -apple-system, "SF Pro Text", system-ui, sans-serif;
|
||||
max-width: 900px; margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 20px; margin: 4px 0 12px; }
|
||||
.meta { color: #9aa3ad; font-size: 13px; margin-bottom: 12px; }
|
||||
.meta code { background: #1e242c; padding: 1px 6px; border-radius: 5px; }
|
||||
#dirty {
|
||||
display: none; background: #3a2b10; border: 1px solid #8a6410; color: #ffd479;
|
||||
padding: 8px 10px; border-radius: 8px; font-size: 13px; margin-bottom: 12px;
|
||||
}
|
||||
#dirty.ok { background: #12331c; border-color: #1f7a3d; color: #7ee2a0; }
|
||||
textarea {
|
||||
width: 100%; min-height: 46vh; resize: vertical;
|
||||
background: #0d0f12; color: #d7dce2; border: 1px solid #2a313a;
|
||||
border-radius: 10px; padding: 12px; font: 13px/1.5 ui-monospace, Menlo, monospace;
|
||||
}
|
||||
.row { display: flex; gap: 10px; margin: 12px 0; flex-wrap: wrap; }
|
||||
button {
|
||||
flex: 1; min-width: 140px; padding: 14px 16px; border: 0; border-radius: 12px;
|
||||
font-size: 16px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
#save { background: #2a313a; color: #e6e8eb; }
|
||||
#saveBuild { background: #0a7d3a; color: #fff; }
|
||||
button:disabled { opacity: .45; }
|
||||
#status {
|
||||
display: none; background: #0d0f12; border: 1px solid #2a313a; border-radius: 10px;
|
||||
padding: 12px; font: 12px/1.5 ui-monospace, Menlo, monospace; white-space: pre-wrap;
|
||||
max-height: 40vh; overflow-y: auto; color: #9aa3ad;
|
||||
}
|
||||
#status.done { border-color: #1f7a3d; color: #d7dce2; }
|
||||
#status.fail { border-color: #8a2b2b; color: #ffb4b4; }
|
||||
#result { display: none; margin-top: 10px; padding: 12px; border-radius: 10px;
|
||||
font-size: 15px; font-weight: 600; }
|
||||
#result.ok { background: #12331c; color: #7ee2a0; }
|
||||
#result.fail { background: #331212; color: #ff8d8d; }
|
||||
#result a { color: #7ec8ff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>⛽ FuelBoard Copy Editor</h1>
|
||||
<div class="meta"><code id="repo">…</code> · <span id="head">…</span></div>
|
||||
<div id="dirty"></div>
|
||||
<textarea id="src" spellcheck="false" placeholder="Loading Localizable.strings…"></textarea>
|
||||
<div class="row">
|
||||
<button id="save">Save</button>
|
||||
<button id="saveBuild">Save & Build</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const KEY = "fbEditorToken";
|
||||
let token = localStorage.getItem(KEY) || "";
|
||||
let pollTimer = null;
|
||||
|
||||
async function api(path, opts = {}) {
|
||||
const headers = { "X-Key": token, ...(opts.headers || {}) };
|
||||
let res = await fetch(path, { ...opts, headers });
|
||||
if (res.status === 401) {
|
||||
token = prompt("Editor token?");
|
||||
if (!token) throw new Error("token required");
|
||||
localStorage.setItem(KEY, token);
|
||||
return api(path, opts); // retry once with the new token
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await api("/file");
|
||||
const data = await res.json();
|
||||
document.getElementById("src").value = data.content;
|
||||
document.getElementById("repo").textContent = data.repo;
|
||||
document.getElementById("head").textContent = data.head;
|
||||
const dirty = document.getElementById("dirty");
|
||||
if (data.dirty.length) {
|
||||
dirty.style.display = "block";
|
||||
dirty.classList.remove("ok");
|
||||
dirty.textContent = "⚠ working tree has uncommitted changes: " + data.dirty.join(", ");
|
||||
} else {
|
||||
dirty.style.display = "block";
|
||||
dirty.classList.add("ok");
|
||||
dirty.textContent = "✓ working tree clean — safe to edit";
|
||||
}
|
||||
} catch (e) { statusBox("error loading file: " + e, "fail"); }
|
||||
}
|
||||
|
||||
function statusBox(text, cls) {
|
||||
const s = document.getElementById("status");
|
||||
s.style.display = "block";
|
||||
s.className = cls || "";
|
||||
s.textContent = text;
|
||||
}
|
||||
|
||||
async function saveOnly() {
|
||||
const res = await api("/save", { method: "POST", body: document.getElementById("src").value });
|
||||
const data = await res.json();
|
||||
if (data.lint_ok) {
|
||||
statusBox("✓ saved — " + data.lint, "done");
|
||||
} else {
|
||||
statusBox("✗ saved but INVALID — " + data.lint, "fail");
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAndBuild() {
|
||||
const saveBtn = document.getElementById("save");
|
||||
const buildBtn = document.getElementById("saveBuild");
|
||||
saveBtn.disabled = buildBtn.disabled = true;
|
||||
document.getElementById("result").style.display = "none";
|
||||
statusBox("saving + starting build…");
|
||||
|
||||
let res = await api("/save-build", { method: "POST", body: document.getElementById("src").value });
|
||||
let data = await res.json();
|
||||
if (!data.job_id) { statusBox("✗ " + JSON.stringify(data), "fail"); enable(); return; }
|
||||
|
||||
// Poll the job until it finishes.
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const r = await api("/job/" + data.job_id);
|
||||
const j = await r.json();
|
||||
statusBox(j.lines.join("\n") + (j.running ? "\n…building…" : ""), j.success ? "done" : "fail");
|
||||
if (j.done) {
|
||||
clearInterval(pollTimer);
|
||||
const result = document.getElementById("result");
|
||||
if (j.success) {
|
||||
result.className = "ok";
|
||||
result.innerHTML = "✓ Build OK — <a href=\"http://192.168.1.131:8765/FuelBoard.ipa\">FuelBoard.ipa</a> " +
|
||||
(j.ipa_size / 1e6).toFixed(2) + " MB";
|
||||
} else {
|
||||
result.className = "fail";
|
||||
result.textContent = "✗ Build failed — see log above";
|
||||
}
|
||||
result.style.display = "block";
|
||||
enable();
|
||||
}
|
||||
} catch (e) { clearInterval(pollTimer); statusBox("poll error: " + e, "fail"); enable(); }
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function enable() {
|
||||
document.getElementById("save").disabled = false;
|
||||
document.getElementById("saveBuild").disabled = false;
|
||||
}
|
||||
|
||||
document.getElementById("save").addEventListener("click", saveOnly);
|
||||
document.getElementById("saveBuild").addEventListener("click", saveAndBuild);
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user