546 lines
26 KiB
Plaintext
546 lines
26 KiB
Plaintext
<!DOCTYPE html>
|
||
<html class="gradient-bg" lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>PokeTube | Customize</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||
<link href="/css/yt-ukraine.svg?v=6" rel="icon">
|
||
<meta content="Poke | Customize" property="og:title">
|
||
<meta content="Theme Poke and add custom scripts — go wild!" property="twitter:description">
|
||
<meta content="https://cdn.glitch.global/d68d17bb-f2c0-4bc3-993f-50902734f652/aa70111e-5bcd-4379-8b23-332a33012b78.image.png?v=1701898829884" property="og:image" />
|
||
<meta content="summary_large_image" name="twitter:card" />
|
||
<meta content="#f97794" name="theme-color" />
|
||
|
||
<!-- your site css -->
|
||
<link href="/css/app-cdn.min.css" rel="stylesheet">
|
||
<link href="/css/app.main.css?v=44600" rel="stylesheet">
|
||
<link href="/css/search.main.css?v=57" rel="stylesheet">
|
||
<link href="/css/watch.main.css" rel="stylesheet">
|
||
|
||
<style>
|
||
@import url("https://p.poketube.fun/https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css");
|
||
|
||
:root{
|
||
--bg:#0c0c0f; --panel:#121218; --panel-2:#0f0f15; --text:#e9ecf1; --muted:#aab2c0;
|
||
--accent:#7cc7ff; --accent-2:#f97794; --ok:#20c997; --warn:#ffb400; --err:#ef476f;
|
||
--border:#212230; --chip:#161824; --chipb:#0f111a; --ring:0 0 0 2px rgba(124,199,255,.25);
|
||
--radius:16px; --radius-lg:22px; --shadow:0 10px 30px rgba(0,0,0,.35);
|
||
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Ubuntu Mono", Consolas, "Liberation Mono", monospace;
|
||
--sans: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, "Helvetica Neue", Arial, "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol", sans-serif;
|
||
--code-size:13.5px;
|
||
}
|
||
|
||
html.gradient-bg{
|
||
min-height:100%;
|
||
background:
|
||
radial-gradient(1200px 800px at 10% 10%, rgba(249,119,148,.22), transparent 60%),
|
||
radial-gradient(1200px 800px at 90% 20%, rgba(124,199,255,.18), transparent 60%),
|
||
radial-gradient(800px 600px at 30% 80%, rgba(124,255,208,.12), transparent 60%),
|
||
linear-gradient(135deg, #0a0a0f, #0b0b12 60%, #0c0c15);
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
*{box-sizing:border-box}
|
||
body{margin:0;color:var(--text);font-family:var(--sans);background:transparent;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;overflow-x:hidden}
|
||
|
||
/* -------- NAVBAR (isolated) -------- */
|
||
.customize-navbar{
|
||
display:flex; align-items:center; gap:16px; padding:12px 20px;
|
||
position:sticky; top:0; z-index:2000; min-height:62px;
|
||
backdrop-filter:blur(12px) saturate(120%);
|
||
background:linear-gradient(180deg, rgba(14,14,24,.78), rgba(14,14,24,.45));
|
||
border-bottom:1px solid rgba(255,255,255,.06);
|
||
}
|
||
.customize-navbar__left{display:flex;align-items:center}
|
||
.customize-navbar__logo{width:124px;display:block}
|
||
.customize-navbar__middle{flex:1; display:flex; justify-content:center; align-items:center; transform:translateY(0)!important}
|
||
.customize-navbar__search{position:relative; width:100%; max-width:640px; transform:translateY(0)!important}
|
||
.customize-navbar__input{
|
||
width:100%; height:40px; padding:0 44px 0 16px;
|
||
border-radius:14px; border:1px solid rgba(255,255,255,.12); outline:none;
|
||
background: radial-gradient(400px 80px at 50% 0%, rgba(124,199,255,.06), transparent 60%), #151720;
|
||
color:var(--text); box-shadow:inset 0 0 0 1px rgba(255,255,255,.02);
|
||
transition:.18s border-color, .18s box-shadow, .18s background-color;
|
||
}
|
||
.customize-navbar__input::placeholder{color:#8b93a6}
|
||
.customize-navbar__input:focus{
|
||
border-color:rgba(124,199,255,.5); box-shadow:0 0 0 4px rgba(124,199,255,.15); background:#141826;
|
||
}
|
||
.customize-navbar__btn{
|
||
position:absolute; right:6px; top:50%;
|
||
transform:translateY(-50%)!important;
|
||
border:0; border-radius:10px; width:36px; height:36px; cursor:pointer;
|
||
background:linear-gradient(180deg,#1b2231,#151b27); color:var(--text);
|
||
display:grid; place-items:center; outline:none;
|
||
}
|
||
.customize-navbar__btn:focus{ box-shadow:0 0 0 3px rgba(124,199,255,.25) }
|
||
.customize-navbar__right{display:flex;align-items:center;gap:10px;padding-left:8px;height:40px}
|
||
.customize-navbar__icon{
|
||
display:grid; place-items:center; width:34px; height:34px; border-radius:10px;
|
||
color:#cfd6e6; border:1px solid rgba(255,255,255,.07);
|
||
background:linear-gradient(180deg,#161925,#121521);
|
||
transition:.15s transform,.15s border-color,.15s background-color,.15s color;
|
||
}
|
||
.customize-navbar__icon:hover{ color:#e8f0ff; transform:translateY(-1px); border-color:rgba(124,199,255,.35) }
|
||
|
||
/* -------- PAGE LAYOUT -------- */
|
||
.container{max-width:1200px;margin:22px auto 64px;padding:0 16px}
|
||
.hero{
|
||
display:grid; grid-template-columns:1.1fr .9fr; gap:16px;
|
||
background:linear-gradient(180deg, rgba(124,199,255,.08), rgba(249,119,148,.07)), var(--panel);
|
||
border:1px solid var(--border); border-radius:var(--radius-lg); padding:18px; box-shadow:var(--shadow);
|
||
}
|
||
@media (max-width:900px){.hero{grid-template-columns:1fr}}
|
||
.title{font-weight:900;font-size:clamp(22px,3vw,34px);letter-spacing:.2px;line-height:1.1}
|
||
.subtitle{color:var(--muted);margin-top:6px;font-size:clamp(13px,1.6vw,15px)}
|
||
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||
.chip{background:linear-gradient(180deg,var(--chip),var(--chipb));border:1px solid var(--border);padding:6px 10px;border-radius:999px;font-size:12px;color:var(--muted)}
|
||
.tabs{display:flex;gap:8px;padding:14px;margin-top:14px;background:var(--panel-2);border:1px solid var(--border);border-radius:var(--radius)}
|
||
.tab{position:relative;padding:10px 14px;border-radius:10px;cursor:pointer;text-transform:uppercase;letter-spacing:.75px;font-size:12px;color:var(--muted);user-select:none;text-decoration:none}
|
||
.tab.active{color:var(--text);background:rgba(124,199,255,.06)}
|
||
.tab.active::after{content:"";position:absolute;left:12px;right:12px;bottom:6px;height:2px;background:linear-gradient(90deg, var(--accent), var(--accent-2));border-radius:2px}
|
||
.work{display:grid;grid-template-columns:1.1fr .9fr;gap:16px;margin-top:14px}
|
||
@media (max-width:1000px){.work{grid-template-columns:1fr}}
|
||
.panel{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
|
||
.panel .head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border);background:linear-gradient(180deg, rgba(255,255,255,.03), transparent)}
|
||
.panel .head .title{font-weight:700;font-size:14px}
|
||
.panel .head .hint{color:var(--muted);font-size:12px}
|
||
.btns{display:flex;gap:8px;flex-wrap:wrap}
|
||
.btn{border:1px solid var(--border);background:var(--chip);color:var(--text);padding:8px 10px;border-radius:10px;font-size:12px;cursor:pointer}
|
||
.btn:hover{border-color:var(--accent);box-shadow:var(--ring)}
|
||
.btn.ok{background:rgba(32,201,151,.12);border-color:rgba(32,201,151,.4)}
|
||
.btn.warn{background:rgba(255,180,0,.12);border-color:rgba(255,180,0,.4)}
|
||
.btn.err{background:rgba(239,71,111,.12);border-color:rgba(239,71,111,.4)}
|
||
|
||
/* -------- Editor (textarea overlayed by Prism) -------- */
|
||
.editor-wrap{position:relative;height:min(70vh,680px);overflow:auto;background:#0a0b12}
|
||
.editor{
|
||
position:absolute; inset:0; resize:none; width:100%; height:100%;
|
||
padding:18px 18px 18px 54px;
|
||
border:0; outline:0; background:transparent; color:transparent;
|
||
caret-color:var(--text);
|
||
font:500 var(--code-size)/1.6 var(--mono);
|
||
white-space:pre; overflow:auto; tab-size:2; -moz-tab-size:2;
|
||
-webkit-text-size-adjust:100%;
|
||
spellcheck:false;
|
||
}
|
||
.hl{
|
||
pointer-events:none; user-select:none;
|
||
padding:18px 18px 18px 54px; min-height:100%;
|
||
font:500 var(--code-size)/1.6 var(--mono);
|
||
white-space:pre; color:var(--text);
|
||
}
|
||
.ln{
|
||
position:absolute; left:0; top:0; bottom:0; width:40px;
|
||
background:#08090f; color:#6272a4; border-right:1px solid #0d0f1a;
|
||
text-align:right; padding:18px 6px; font:500 var(--code-size)/1.6 var(--mono);
|
||
user-select:none;
|
||
}
|
||
|
||
.preview{padding:14px;background:var(--panel-2)}
|
||
.preview .box{border:1px dashed var(--border);border-radius:12px;padding:14px;background:#0b0d14;min-height:180px}
|
||
|
||
footer{max-width:1200px;margin:40px auto 50px;padding:0 16px;color:var(--muted);font-size:12px}
|
||
.btn:focus,.tab:focus{outline:none;box-shadow:var(--ring)}
|
||
</style>
|
||
|
||
<!-- Prism (through your proxy to respect CSP) -->
|
||
<link rel="stylesheet" href="https://p.poketube.fun/https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-tomorrow.min.css">
|
||
<style>
|
||
.hl code[class*="language-"]{
|
||
background:transparent!important;
|
||
font:500 var(--code-size)/1.6 var(--mono);
|
||
color:var(--text); white-space:pre;
|
||
}
|
||
</style>
|
||
<script src="https://p.poketube.fun/https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js"></script>
|
||
<script src="https://p.poketube.fun/https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
|
||
<script>
|
||
Prism.plugins.autoloader.languages_path = 'https://p.poketube.fun/https://cdn.jsdelivr.net/npm/prismjs@1/components/';
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<div class="app gradient-bg">
|
||
<!-- NEW NAVBAR -->
|
||
<nav class="customize-navbar">
|
||
<div class="customize-navbar__left">
|
||
<a href="/143"><img class="customize-navbar__logo" src="/css/logo.svg?v=5" alt="PokeTube"></a>
|
||
</div>
|
||
<div class="customize-navbar__middle">
|
||
<form class="customize-navbar__search" action="/search">
|
||
<input id="fname" name="query" class="customize-navbar__input" type="search" placeholder="Search videos, channels…" aria-label="Search"/>
|
||
<button class="customize-navbar__btn" type="submit" aria-label="Search"><i class="fa-light fa-search"></i></button>
|
||
</form>
|
||
</div>
|
||
<div class="customize-navbar__right">
|
||
<a class="customize-navbar__icon" href="/domains" aria-label="Domains"><i class="fa-light fa-server"></i></a>
|
||
<a class="customize-navbar__icon" href="/privacy" aria-label="Privacy"><i class="fa-light fa-shield"></i></a>
|
||
<a class="customize-navbar__icon" href="/video/upload?from=" aria-label="Upload"><i class="fa-light fa-video"></i></a>
|
||
<a class="customize-navbar__icon" href="https://codeberg.org/Ashley/poketube/issues" aria-label="Report a bug"><i class="fa-light fa-bug"></i></a>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container">
|
||
<section class="hero">
|
||
<div>
|
||
<div class="title">Customize Poke</div>
|
||
<div class="subtitle">Personalize everything about poke!!!</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="tabs" role="tablist" aria-label="Customization Tabs">
|
||
<% if (!tab) { %>
|
||
<a class="tab active" role="tab" aria-selected="true" href="/customize">Custom CSS</a>
|
||
<a class="tab" role="tab" aria-selected="false" href="/customize?tab=js">Custom JS</a>
|
||
<% } else { %>
|
||
<a class="tab" role="tab" aria-selected="false" href="/customize">Custom CSS</a>
|
||
<a class="tab active" role="tab" aria-selected="true" href="/customize?tab=js">Custom JS</a>
|
||
<% } %>
|
||
</div>
|
||
|
||
<% if (!tab) { %>
|
||
<!-- ---------------- CSS TAB ---------------- -->
|
||
<section class="work" aria-label="Custom CSS workspace">
|
||
<div class="panel">
|
||
<div class="head">
|
||
<div>
|
||
<div class="title">Custom CSS Editor</div>
|
||
<div class="hint">Saved to <code>localStorage['poke-custom-css']</code></div>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn ok" id="saveCssBtn"><i class="fa-light fa-floppy-disk"></i> Save</button>
|
||
<button class="btn" id="formatCssBtn">Format</button>
|
||
<button class="btn" id="wrapCssBtn" data-wrap="1">Wrap: On</button>
|
||
<button class="btn" id="copyCssBtn">Copy</button>
|
||
<button class="btn warn" id="exportCssBtn">Export</button>
|
||
<label class="btn warn" for="importCssFile" style="cursor:pointer"><i class="fa-light fa-file-arrow-up"></i> Import
|
||
<input id="importCssFile" type="file" accept=".css,text/css,text/plain" style="display:none">
|
||
</label>
|
||
<button class="btn err" id="resetCssBtn">Reset</button>
|
||
</div>
|
||
</div>
|
||
<div class="editor-wrap" id="cssWrap" aria-label="CSS code editor">
|
||
<pre class="ln" id="cssLines">1</pre>
|
||
<pre class="hl" id="cssHl"></pre>
|
||
<textarea id="cssEd" class="editor" spellcheck="false" aria-label="Custom CSS"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="head">
|
||
<div>
|
||
<div class="title">Live Preview</div>
|
||
<div class="hint">Applies your CSS below (isolated box)</div>
|
||
</div>
|
||
<div class="btns"><button class="btn" id="reloadPreviewCss"><i class="fa-light fa-rotate"></i> Reload</button></div>
|
||
</div>
|
||
<div class="preview">
|
||
<div class="box" id="cssPreviewBox">
|
||
<p style="margin-top:0">Preview area — try:</p>
|
||
<pre style="margin:0" aria-hidden="true">nav{backdrop-filter:blur(16px)} .tab.active{filter:drop-shadow(0 0 10px #7cc7ff)}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<% } %>
|
||
|
||
<% if (tab) { %>
|
||
<!-- ---------------- JS TAB ---------------- -->
|
||
<section class="work" aria-label="Custom JS workspace">
|
||
<div class="panel">
|
||
<div class="head">
|
||
<div>
|
||
<div class="title">Custom JS Editor</div>
|
||
<div class="hint">Saved to <code>localStorage['poke-custom-script']</code></div>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn ok" id="saveJsBtn"><i class="fa-light fa-floppy-disk"></i> Save</button>
|
||
<button class="btn" id="formatJsBtn">Format</button>
|
||
<button class="btn" id="wrapJsBtn" data-wrap="1">Wrap: On</button>
|
||
<button class="btn" id="copyJsBtn">Copy</button>
|
||
<button class="btn warn" id="exportJsBtn">Export</button>
|
||
<label class="btn warn" for="importJsFile" style="cursor:pointer"><i class="fa-light fa-file-arrow-up"></i> Import
|
||
<input id="importJsFile" type="file" accept=".js,application/javascript,text/javascript,text/plain" style="display:none">
|
||
</label>
|
||
<button class="btn err" id="resetJsBtn">Reset</button>
|
||
</div>
|
||
</div>
|
||
<div class="editor-wrap" id="jsWrap" aria-label="JS code editor">
|
||
<pre class="ln" id="jsLines">1</pre>
|
||
<pre class="hl" id="jsHl"></pre>
|
||
<textarea id="jsEd" class="editor" spellcheck="false" aria-label="Custom JS"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="head"><div class="title">Security Notice</div><div class="hint">Run only code you wrote or fully trust.</div></div>
|
||
<div class="preview">
|
||
<div class="box">
|
||
<p style="margin-top:0">Snippet idea:</p>
|
||
<pre style="margin:0"><code>// Add a badge to the navbar
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const nav = document.querySelector('.customize-navbar__right');
|
||
if (!nav) return;
|
||
const b = document.createElement('span');
|
||
b.textContent = 'Custom JS Active';
|
||
b.style.cssText = 'margin-left:8px;padding:4px 8px;border-radius:999px;background:#1a2a40;color:#a8dcff;font:600 11px/1 var(--sans)';
|
||
nav.appendChild(b);
|
||
});</code></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<% } %>
|
||
</div>
|
||
|
||
<footer>
|
||
<div>© 2021–2025 PokeTube • Free software under the GNU GPL. Customizations are stored locally on your device.</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<!-- ================= SCRIPTS ================ -->
|
||
<script>
|
||
/* ---------- utils ---------- */
|
||
const $ = s => document.querySelector(s);
|
||
const on = (el, ev, fn) => el && el.addEventListener(ev, fn, { passive:true });
|
||
const saveFile = (name, content, type='text/plain') => {
|
||
const blob = new Blob([content], { type });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url; a.download = name; document.body.appendChild(a); a.click();
|
||
URL.revokeObjectURL(url); a.remove();
|
||
};
|
||
const copyText = async (txt) => { try{ await navigator.clipboard.writeText(txt); toast('Copied'); } catch{ toast('Copy failed', true); } };
|
||
const toast = (msg,bad=false)=>{
|
||
const t=document.createElement('div');
|
||
t.textContent=msg;
|
||
t.style.cssText='position:fixed;left:50%;bottom:22px;transform:translateX(-50%);background:'+(bad?'#4d1622':'#132a20')+';color:#fff;padding:10px 14px;border:1px solid rgba(255,255,255,.12);border-radius:12px;z-index:9999';
|
||
document.body.appendChild(t); setTimeout(()=>t.remove(),1500);
|
||
};
|
||
const debounce = (fn,ms=120)=>{ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a),ms); }; };
|
||
|
||
/* ---------- quick formatters ---------- */
|
||
const basicFormatCSS = s => { try{ return s.replace(/\{/g,'{\n ').replace(/\;/g,';\n ').replace(/\}\s*/g,'\n}\n').replace(/\n\s+\n/g,'\n'); }catch{ return s } };
|
||
const basicFormatJS = s => { try{ return s.replace(/;\s*/g,';\n').replace(/\{\s*/g,'{\n ').replace(/\}\s*/g,'\n}\n').replace(/\)\s*\{/g,') {\n ').replace(/\n\s+\n/g,'\n'); }catch{ return s } };
|
||
|
||
/* ---------- clean old polluted saves (from pre-Prism versions) ---------- */
|
||
function stripHighlightArtifacts(s){
|
||
if (typeof s !== 'string') return s;
|
||
if (!/<span\s+class="tok\s/.test(s) && !/<span\s+class="tok\s/.test(s)) return s;
|
||
let out = s.replace(/<\/span>/g, '').replace(/<span\s+class="tok\s[^>]*>/g, '');
|
||
out = out.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');
|
||
return out;
|
||
}
|
||
|
||
/* ---------- Prism-powered renderer ---------- */
|
||
function prismRender(preEl, lang, text){
|
||
preEl.innerHTML = '';
|
||
const code = document.createElement('code');
|
||
code.className = 'language-' + (lang==='css' ? 'css' : 'javascript');
|
||
code.textContent = text; // important: no innerHTML to avoid XSS / artifacts
|
||
preEl.appendChild(code);
|
||
if (window.Prism) {
|
||
Prism.highlightElement(code);
|
||
}
|
||
}
|
||
|
||
/* ---------- Editor factory (overlayed Prism) ---------- */
|
||
function makeEditor(edSel, hlSel, lnSel, { lang='css', storageKey, defaultValue='' }){
|
||
const ed = $(edSel), hl = $(hlSel), ln = $(lnSel);
|
||
|
||
const load = () => {
|
||
let saved = localStorage.getItem(storageKey);
|
||
if (saved === null) saved = defaultValue;
|
||
const cleaned = stripHighlightArtifacts(saved);
|
||
if (cleaned !== saved) { localStorage.setItem(storageKey, cleaned); saved = cleaned; }
|
||
return saved;
|
||
};
|
||
|
||
let lastLineCount = 1;
|
||
const setLines = (text) => {
|
||
const lines = text.split('\n').length;
|
||
if (lines === lastLineCount) return;
|
||
lastLineCount = lines;
|
||
let out = '';
|
||
for (let i=1;i<=lines;i++) out += i + (i<lines ? '\n' : '');
|
||
ln.textContent = out;
|
||
};
|
||
|
||
const render = (text) => { setLines(text); prismRender(hl, lang, text); };
|
||
|
||
// initial
|
||
const initial = load();
|
||
ed.value = initial; render(initial);
|
||
|
||
// scroll sync
|
||
const syncScroll = () => { hl.scrollTop = ed.scrollTop; hl.scrollLeft = ed.scrollLeft; ln.scrollTop = ed.scrollTop; };
|
||
on(ed, 'scroll', syncScroll);
|
||
|
||
// input update (debounced + rAF for smoothness)
|
||
const update = () => { window.requestAnimationFrame(()=> render(ed.value)); };
|
||
on(ed, 'input', debounce(update, 40));
|
||
|
||
// focus overlay click sends focus to textarea
|
||
on(hl, 'mousedown', () => ed.focus());
|
||
|
||
// keyboard niceties: Tab / Shift+Tab, Cmd/Ctrl+/, Home/End preserve column
|
||
on(ed, 'keydown', (e)=>{
|
||
const { selectionStart:s, selectionEnd:ePos, value:v } = ed;
|
||
|
||
// Save (Ctrl/Cmd+S)
|
||
if ((e.metaKey||e.ctrlKey) && e.key.toLowerCase()==='s'){ e.preventDefault(); save(); return; }
|
||
|
||
// Toggle comment (JS only)
|
||
if (lang==='js' && (e.metaKey||e.ctrlKey) && e.key === '/'){
|
||
e.preventDefault();
|
||
const lines = v.slice(s,ePos).split('\n');
|
||
const allCommented = lines.every(l=>l.trimStart().startsWith('//'));
|
||
const replaced = lines.map(l=>{
|
||
if (allCommented) return l.replace(/^\s*\/\/\s?/, '');
|
||
return l.replace(/^/, '// ');
|
||
}).join('\n');
|
||
replaceRange(s, ePos, replaced);
|
||
return;
|
||
}
|
||
|
||
// Tab / Shift+Tab
|
||
if (e.key === 'Tab'){
|
||
e.preventDefault();
|
||
const sel = v.slice(s,ePos);
|
||
if (s !== ePos && sel.includes('\n')){
|
||
// multi-line indent/outdent
|
||
const lines = sel.split('\n');
|
||
const out = e.shiftKey
|
||
? lines.map(l=>l.replace(/^ {1,2}/,'')).join('\n')
|
||
: lines.map(l=>' '+l).join('\n');
|
||
replaceRange(s, ePos, out);
|
||
} else {
|
||
if (e.shiftKey){
|
||
// outdent single line
|
||
const startLine = v.lastIndexOf('\n', s-1)+1;
|
||
const m = /^ {1,2}/.exec(v.slice(startLine));
|
||
if (m) replaceRange(startLine, startLine+m[0].length, '');
|
||
} else {
|
||
insertAt(s, ' ');
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
});
|
||
|
||
function insertAt(pos, text){
|
||
const val = ed.value;
|
||
ed.value = val.slice(0,pos) + text + val.slice(pos);
|
||
const np = pos + text.length;
|
||
ed.selectionStart = ed.selectionEnd = np;
|
||
update();
|
||
}
|
||
function replaceRange(start, end, text){
|
||
const val = ed.value;
|
||
ed.value = val.slice(0,start) + text + val.slice(end);
|
||
ed.selectionStart = start;
|
||
ed.selectionEnd = start + text.length;
|
||
update();
|
||
}
|
||
|
||
// drag & drop import
|
||
['dragenter','dragover'].forEach(evt => on(ed, evt, e=>{ e.preventDefault(); }));
|
||
on(ed, 'drop', e=>{
|
||
e.preventDefault();
|
||
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||
if (!f) return;
|
||
const r = new FileReader();
|
||
r.onload = () => { ed.value = String(r.result||''); update(); save(); };
|
||
r.readAsText(f);
|
||
});
|
||
|
||
// actions
|
||
const save = () => { localStorage.setItem(storageKey, ed.value); toast('Saved'); };
|
||
const reset = () => { if (confirm('Clear the editor?')) { ed.value=''; update(); save(); } };
|
||
const copy = () => copyText(ed.value);
|
||
const exportFile = () => saveFile(storageKey + (lang==='css'?'.css':'.js'), ed.value, 'text/plain');
|
||
|
||
return {
|
||
ed, hl, ln, save, reset, copy, exportFile, render, setLines,
|
||
setWrap:(onwrap)=>{ ed.style.whiteSpace = onwrap?'pre-wrap':'pre'; ed.style.overflowWrap = onwrap?'anywhere':'normal'; },
|
||
format:()=>{ if (lang==='css') ed.value = basicFormatCSS(ed.value); else ed.value = basicFormatJS(ed.value); update(); },
|
||
importFrom:(file)=> new Promise(res=>{
|
||
const r=new FileReader();
|
||
r.onload=()=>{ ed.value=String(r.result||''); update(); res(); };
|
||
r.readAsText(file);
|
||
})
|
||
};
|
||
}
|
||
|
||
/* ---------- init per tab ---------- */
|
||
<% if (!tab) { %>
|
||
const cssEditor = makeEditor('#cssEd','#cssHl','#cssLines',{
|
||
lang:'css', storageKey:'poke-custom-css',
|
||
defaultValue:`/* Write CSS that applies across Poke. Example:
|
||
|
||
nav { backdrop-filter: blur(16px) saturate(120%); }
|
||
.tab.active { filter: drop-shadow(0 0 10px #7cc7ff); }
|
||
:root { --accent: #9df7c9; }
|
||
|
||
*/`
|
||
});
|
||
|
||
// live preview
|
||
const cssStylePreview = document.createElement('style');
|
||
cssStylePreview.id='custom-css-preview'; document.body.appendChild(cssStylePreview);
|
||
const applyCssPreview = () => { cssStylePreview.textContent = cssEditor.ed.value || ''; };
|
||
applyCssPreview();
|
||
|
||
on($('#saveCssBtn'),'click',cssEditor.save);
|
||
on($('#formatCssBtn'),'click',cssEditor.format);
|
||
on($('#copyCssBtn'),'click',cssEditor.copy);
|
||
on($('#exportCssBtn'),'click',cssEditor.exportFile);
|
||
on($('#resetCssBtn'),'click',cssEditor.reset);
|
||
on($('#importCssFile'),'change', async e=>{ const f=e.target.files?.[0]; if(f) await cssEditor.importFrom(f); });
|
||
on($('#wrapCssBtn'),'click', e=>{
|
||
const b=e.currentTarget; const onw=b.getAttribute('data-wrap')==='1';
|
||
cssEditor.setWrap(!onw); b.setAttribute('data-wrap', onw?'0':'1'); b.textContent='Wrap: ' + (onw?'Off':'On');
|
||
});
|
||
on($('#reloadPreviewCss'),'click',applyCssPreview);
|
||
on(cssEditor.ed,'input',debounce(applyCssPreview,50));
|
||
<% } %>
|
||
|
||
<% if (tab) { %>
|
||
const jsEditor = makeEditor('#jsEd','#jsHl','#jsLines',{
|
||
lang:'js', storageKey:'poke-custom-script',
|
||
defaultValue:`// Write JavaScript that will run on Poke pages.
|
||
// Tip: wrap in DOMContentLoaded to avoid race conditions:
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
console.log('[Poke Custom] Hello from your script!');
|
||
});`
|
||
});
|
||
|
||
on($('#saveJsBtn'),'click',jsEditor.save);
|
||
on($('#formatJsBtn'),'click',jsEditor.format);
|
||
on($('#copyJsBtn'),'click',jsEditor.copy);
|
||
on($('#exportJsBtn'),'click',jsEditor.exportFile);
|
||
on($('#resetJsBtn'),'click',jsEditor.reset);
|
||
on($('#importJsFile'),'change', async e=>{ const f=e.target.files?.[0]; if(f) await jsEditor.importFrom(f); });
|
||
on($('#wrapJsBtn'),'click', e=>{
|
||
const b=e.currentTarget; const onw=b.getAttribute('data-wrap')==='1';
|
||
jsEditor.setWrap(!onw); b.setAttribute('data-wrap', onw?'0':'1'); b.textContent='Wrap: ' + (onw?'Off':'On');
|
||
});
|
||
<% } %>
|
||
|
||
// global: Ctrl/Cmd+S saves current visible editor
|
||
on(document,'keydown',e=>{
|
||
if ((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='s'){
|
||
e.preventDefault();
|
||
const cssBtn = document.querySelector('#saveCssBtn');
|
||
const jsBtn = document.querySelector('#saveJsBtn');
|
||
if (cssBtn && getComputedStyle(cssBtn).display !== 'none') cssBtn.click();
|
||
if (jsBtn && getComputedStyle(jsBtn).display !== 'none') jsBtn.click();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<!-- site loader to apply saved CSS/JS globally -->
|
||
<script src="/css/custom-css.js"></script>
|
||
</body>
|
||
</html>
|