poke/html/priv.ejs
2025-08-19 12:46:45 +02:00

758 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
This Source Code Form is subject to the terms of the GNU General Public License:
Copyright (C) 2021-2025 POKETUBE (https://codeberg.org/Ashley/poketube)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses/.
-->
<!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 (safely)!" 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" />
<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;
}
/* Top nav — fixed spacing + centered search */
nav{
display:flex;
align-items:center;
gap:16px;
padding:12px 20px;
position:sticky;
top:0;
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);
z-index:2000;
min-height:62px;
}
nav .left{ display:flex; align-items:center }
nav .left img{ width:124px; display:block }
nav .middle{
flex:1;
display:flex;
justify-content:center;
align-items:center;
}
nav .middle .search{ position:relative; width:100%; max-width:640px }
nav .middle input[type="search"]{
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);
transition:.18s border-color, .18s box-shadow, .18s background-color;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.02);
}
nav .middle input[type="search"]::placeholder{ color:#8b93a6 }
nav .middle input[type="search"]:focus{
border-color: rgba(124,199,255,.5);
box-shadow: 0 0 0 4px rgba(124,199,255,.15);
background:#141826;
}
nav .middle button{
position:absolute; right:6px; top:50%; transform:translateY(-50%);
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;
}
nav .middle button:focus{ box-shadow:0 0 0 3px rgba(124,199,255,.25) }
nav .right{ display:flex; align-items:center; gap:14px; padding-left:8px }
nav .right a{
color:#cfd6e6; opacity:.9; display:grid; place-items:center;
width:34px; height:34px; border-radius:10px; 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;
}
nav .right a:hover{ color:#e8f0ff; transform:translateY(-1px); border-color:rgba(124,199,255,.35) }
/* Page container */
.container{
max-width:1200px;
margin:22px auto 64px;
padding:0 16px;
}
/* Header card */
.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 } }
.hero .title{
font-weight:900; font-size:clamp(22px, 3vw, 34px);
letter-spacing:.2px; line-height:1.1;
}
.hero .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);
}
/* Theme selector row */
.theme-row{
display:flex; gap:10px; align-items:center; margin-top:10px;
flex-wrap:wrap;
}
.theme-row select{
background:var(--panel-2); color:var(--text);
border:1px solid var(--border); border-radius:10px; padding:8px 10px;
}
/* Tabs */
.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;
}
/* Editor layout */
.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 highlighted pre) */
.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;
}
.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;
}
/* Syntax highlighting (no library) */
.tok.comment { color:#6c7891; font-style:italic }
.tok.str { color:#a1eafb }
.tok.num { color:#ffd580 }
.tok.kw { color:#8ef0a5; font-weight:600 }
.tok.fn { color:#9bb7ff }
.tok.prop { color:#c2d1ff }
.tok.op { color:#ffadb5 }
.tok.selector{ color:#ffb8e2 }
.tok.at { color:#9cf1dd }
.tok.bool { color:#ffcf99 }
.tok.reg { color:#a5ffce }
/* Preview */
.preview{
padding:14px; background:var(--panel-2);
}
.preview .box{
border:1px dashed var(--border);
border-radius:12px; padding:14px; background:#0b0d14;
min-height:180px;
}
/* Alerts */
.alert{
display:flex; align-items:flex-start; gap:10px;
padding:10px 12px; border-radius:12px; border:1px solid var(--border);
background:linear-gradient(180deg, rgba(255,255,255,.02), transparent);
font-size:13px; color:var(--muted);
}
.alert i{ margin-top:2px }
.alert.ok{ border-color:rgba(32,201,151,.35) }
.alert.warn{ border-color:rgba(255,180,0,.35) }
.alert.err{ border-color:rgba(239,71,111,.35) }
/* Footer */
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>
<!-- Theme runtime style (updated by JS) -->
<style id="theme-style"></style>
</head>
<body>
<div class="app gradient-bg">
<nav>
<div class="left">
<a class="class" href="/143" style="color:#fff">
<img src="/css/logo.svg?v=5" alt="PokeTube" />
</a>
</div>
<div class="middle">
<div class="search">
<form action="/search">
<input class="search-bar" autocomplete="on" id="fname" name="query" placeholder="Search videos, channels…" aria-label="Search" />
<button class="btn btn-success" type="submit" aria-label="Search"><i class="fa-light fa-search"></i></button>
</form>
<img src="https://search-metrics.poketube.fun/t/rep.gif" style="border:0;width:0;visibility:hidden" alt="">
</div>
</div>
<div class="right">
<a href="/domains" aria-label="Domains"><i class="fa-light fa-server"></i></a>
<a href="/privacy" aria-label="Privacy"><i class="fa-light fa-shield"></i></a>
<a href="/video/upload?from=" aria-label="Upload"><i class="fa-light fa-video"></i></a>
<a 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 styles and behavior. Your edits are stored locally in your browser. Nothing is uploaded.</div>
<div class="theme-row">
<label for="themeSelect">Preset theme:</label>
<select id="themeSelect" aria-label="Preset CSS theme">
<option value="midnight">Midnight (default)</option>
<option value="sakura">Sakura</option>
<option value="ocean">Ocean</option>
<option value="solarized">Solarized Dark</option>
<option value="amoled">AMOLED Black</option>
<option value="highcontrast">High Contrast</option>
</select>
<button class="btn" id="saveThemeBtn" title="Save theme">Save Theme</button>
<button class="btn warn" id="resetThemeBtn" title="Reset theme">Reset Theme</button>
</div>
<div class="chips" role="list" style="margin-top:10px">
<span class="chip"><i class="fa-light fa-lock"></i>&nbsp; Local-only storage</span>
<span class="chip"><i class="fa-light fa-floppy-disk"></i>&nbsp; Auto-save</span>
<span class="chip"><i class="fa-light fa-code"></i>&nbsp; Syntax highlight (no libs)</span>
<span class="chip"><i class="fa-light fa-bolt"></i>&nbsp; Live preview</span>
</div>
</div>
<div class="alert ok" role="status" aria-live="polite">
<i class="fa-light fa-shield-check"></i>
<div>
<strong>Heads-up:</strong> Your CSS/JS never leaves the browser. If you clear site data, your customizations are removed. Export a backup first.
</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" title="Save (Ctrl/Cmd+S)"><i class="fa-light fa-floppy-disk"></i> Save</button>
<button class="btn" id="formatCssBtn" title="Basic formatting">Format</button>
<button class="btn" id="wrapCssBtn" data-wrap="1" title="Toggle line wrap">Wrap: On</button>
<button class="btn" id="copyCssBtn" title="Copy to clipboard">Copy</button>
<button class="btn warn" id="exportCssBtn" title="Download a backup">Export</button>
<label class="btn warn" for="importCssFile" title="Restore from file" 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" title="Clear editor">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 something like:</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" title="Save (Ctrl/Cmd+S)"><i class="fa-light fa-floppy-disk"></i> Save</button>
<button class="btn" id="formatJsBtn" title="Basic formatting">Format</button>
<button class="btn" id="wrapJsBtn" data-wrap="1" title="Toggle line wrap">Wrap: On</button>
<button class="btn" id="copyJsBtn" title="Copy to clipboard">Copy</button>
<button class="btn warn" id="exportJsBtn" title="Download a backup">Export</button>
<label class="btn warn" for="importJsFile" title="Restore from file" 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" title="Clear editor">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="alert warn">
<i class="fa-light fa-triangle-exclamation"></i>
<div>
Your script executes on Poke pages via the sites loader. If something breaks, use a private window or clear site storage to recover.
</div>
</div>
<div style="height:10px"></div>
<div class="box">
<p style="margin-top:0">Snippet ideas:</p>
<pre style="margin:0"><code>// Example: add a badge to nav
document.addEventListener('DOMContentLoaded', () => {
const nav = document.querySelector('nav .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>© 20212025 PokeTube • Free software under the GNU GPL. Customizations are stored locally on your device.</div>
</footer>
</div>
<!-- ============================== SCRIPTS =============================== -->
<script>
// --------------------- Utilities ---------------------
const $ = sel => document.querySelector(sel);
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=200) => { let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), ms); }; };
// --------------------- Minimal formatter ---------------------
function basicFormatCSS(src){
try{
return src
.replace(/\{/g,'{\n ')
.replace(/\;/g,';\n ')
.replace(/\}\s*/g,'\n}\n')
.replace(/\n\s+\n/g,'\n');
}catch{ return src }
}
function basicFormatJS(src){
try{
return src
.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 src }
}
// --------------------- Syntax highlighter (no libraries) ---------------------
// IMPORTANT: use single backslashes in regex literals (previous version double-escaped, breaking highlighting)
const JS_KEYWORDS = /\b(?:await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|from|function|if|import|in|instanceof|let|new|null|return|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/g;
const JS_BUILTINS = /\b(?:Array|Object|String|Number|Boolean|Map|Set|WeakMap|WeakSet|Date|Math|JSON|Promise|RegExp|Error|TypeError|URL|Node|Element|document|window|console|fetch)\b/g;
function escapeHTML(s){ return s.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
function highlightJS(code){
let s = escapeHTML(code);
// comments
s = s.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="tok comment">$1</span>');
s = s.replace(/(\/\/.*?$)/gm, '<span class="tok comment">$1</span>');
// strings + template literals
s = s.replace(/(`[\s\S]*?`)/g, '<span class="tok str">$1</span>');
s = s.replace(/('[^'\\]*(?:\\.[^'\\]*)*')/g, '<span class="tok str">$1</span>');
s = s.replace(/("[^"\\]*(?:\\.[^"\\]*)*")/g, '<span class="tok str">$1</span>');
// regex literals (rough)
s = s.replace(/(\/(?:\\.|[^\\\/\n])+\/[gimsuy]*)/g, '<span class="tok reg">$1</span>');
// numbers/booleans/null
s = s.replace(/\b(0x[\da-fA-F]+|\d+\.\d+|\d+)\b/g, '<span class="tok num">$1</span>');
s = s.replace(/\b(true|false|null)\b/g, '<span class="tok bool">$1</span>');
// keywords & builtins
s = s.replace(JS_KEYWORDS, '<span class="tok kw">$&</span>');
s = s.replace(JS_BUILTINS, '<span class="tok fn">$&</span>');
// operators
s = s.replace(/([=+\-/*<>!&|%^~?:]+)/g, '<span class="tok op">$1</span>');
return s;
}
function highlightCSS(code){
let s = escapeHTML(code);
// block comments
s = s.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="tok comment">$1</span>');
// at-rules
s = s.replace(/(@[a-zA-Z-]+)/g, '<span class="tok at">$1</span>');
// selectors (naive: line before { )
s = s.replace(/^([^{}@\n][^{\n]+)(?=\s*\{)/gm, '<span class="tok selector">$1</span>');
// properties/values
s = s.replace(/([a-zA-Z-]+)(\s*:\s*)([^;}{]+)/g, (m, p, c, v) => {
v = v.replace(/(#[0-9a-fA-F]{3,8}|\b\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw)?\b)/g, '<span class="tok num">$1</span>');
v = v.replace(/("[^"]*"|'[^']*')/g, '<span class="tok str">$1</span>');
return '<span class="tok prop">'+p+'</span>'+c+v;
});
return s;
}
function makeEditor(edSel, hlSel, lnSel, { lang='css', storageKey, defaultValue='' }){
const ed = $(edSel), hl = $(hlSel), ln = $(lnSel);
const load = () => {
const saved = localStorage.getItem(storageKey);
return (saved !== null) ? saved : defaultValue;
};
const setLines = (text) => {
const lines = text.split('\n').length;
let out = '';
for (let i=1;i<=lines;i++) out += i + (i<lines ? '\n' : '');
ln.textContent = out;
};
const render = (text) => {
setLines(text);
hl.innerHTML = (lang==='css') ? highlightCSS(text) : highlightJS(text);
};
const initial = load();
ed.value = initial;
render(initial);
const syncScroll = () => { hl.scrollTop = ed.scrollTop; hl.scrollLeft = ed.scrollLeft; ln.scrollTop = ed.scrollTop; };
on(ed, 'scroll', syncScroll);
const update = () => render(ed.value);
on(ed, 'input', debounce(update, 20));
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);
})
};
}
// --------------------- Theme presets ---------------------
const THEME_STYLE = $('#theme-style');
const THEME_KEY = 'poke-custom-theme';
const THEMES = {
midnight: `
:root{
--bg:#0c0c0f; --panel:#121218; --panel-2:#0f0f15; --text:#e9ecf1; --muted:#aab2c0;
--accent:#7cc7ff; --accent-2:#f97794; --border:#212230; --chip:#161824; --chipb:#0f111a;
}`,
sakura: `
:root{
--bg:#0e0a10; --panel:#1b1019; --panel-2:#160d14; --text:#ffeef4; --muted:#ffc8d8;
--accent:#ff9cc3; --accent-2:#ffd1e1; --border:#2a1622; --chip:#22111b; --chipb:#1a0d15;
}`,
ocean: `
:root{
--bg:#071017; --panel:#0b1b27; --panel-2:#091521; --text:#e6f3ff; --muted:#a9c4da;
--accent:#53b7ff; --accent-2:#3de1c5; --border:#102536; --chip:#0c1a26; --chipb:#0a1520;
}`,
solarized: `
:root{
--bg:#002b36; --panel:#073642; --panel-2:#062c35; --text:#eee8d5; --muted:#93a1a1;
--accent:#268bd2; --accent-2:#2aa198; --border:#0a3945; --chip:#0b2f38; --chipb:#0a2830;
}`,
amoled: `
:root{
--bg:#000; --panel:#050505; --panel-2:#0a0a0a; --text:#f2f2f2; --muted:#bdbdbd;
--accent:#8ab4ff; --accent-2:#ff8ab4; --border:#111; --chip:#0b0b0b; --chipb:#070707;
}`,
highcontrast: `
:root{
--bg:#000; --panel:#000; --panel-2:#000; --text:#fff; --muted:#e6e6e6;
--accent:#00ffff; --accent-2:#ff00ff; --border:#fff; --chip:#000; --chipb:#000;
}`
};
function applyTheme(name){
const css = THEMES[name] || THEMES.midnight;
THEME_STYLE.textContent = css;
$('#themeSelect').value = name in THEMES ? name : 'midnight';
}
function saveTheme(){ localStorage.setItem(THEME_KEY, $('#themeSelect').value); toast('Theme saved'); }
function resetTheme(){ localStorage.removeItem(THEME_KEY); applyTheme('midnight'); toast('Theme reset'); }
// --------------------- Initialize per tab ---------------------
// Theme boot
applyTheme(localStorage.getItem(THEME_KEY) || 'midnight');
on($('#themeSelect'), 'change', (e)=> applyTheme(e.target.value));
on($('#saveThemeBtn'), 'click', saveTheme);
on($('#resetThemeBtn'), 'click', resetTheme);
// CSS 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; }
*/`
});
const cssStylePreview = document.createElement('style');
cssStylePreview.id = 'custom-css-preview';
document.body.appendChild(cssStylePreview);
const applyCssPreview = () => { cssStylePreview.textContent = cssEditor.ed.value || ''; };
applyCssPreview();
// Controls
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);
// Live preview while typing
on(cssEditor.ed, 'input', debounce(applyCssPreview, 50));
// Save on Ctrl/Cmd+S
on(document, 'keydown', (e)=>{
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase()==='s') { e.preventDefault(); cssEditor.save(); }
});
<% } %>
// JS TAB
<% 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');
});
on(document, 'keydown', (e)=>{
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase()==='s') { e.preventDefault(); jsEditor.save(); }
});
<% } %>
</script>
<!-- Site loader that applies saved CSS/JS globally (kept for compatibility) -->
<script src="/css/custom-css.js"></script>
</body>
</html>