- DD cycle data fetching - ShaiWatcher will now keep an updated loot table of the unique items in the DD each week The bot will **only** edit its message if already present, which should reduce message spam - Added command `/dd_update` to control the update behaviour. stop|resume|start [reason_text] - Docsite changes - Added "ADMIN" tags to commands, signifying owner-only commands - Owner-only commands are now filtered under the "moderator" category - Added docs for `/dd_update` - Logging - Added logging info for more verbose info relating to configuration and installation
569 lines
26 KiB
HTML
569 lines
26 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
||
|
||
<!-- Favicons (put files in /assets/docs/) -->
|
||
<link rel="icon" type="image/png" href="/assets/docs/favicon.png">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/docs/favicon-32x32.png">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/docs/favicon-16x16.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/docs/apple-touch-icon.png">
|
||
<link rel="manifest" href="/assets/docs/site.webmanifest">
|
||
|
||
<title>__TITLE__</title>
|
||
<style>
|
||
:root { --bg:#0b0f14; --panel:#121922; --muted:#6b7280; --fg:#e5e7eb; --accent:#60a5fa; --sticky-top: 8px; --header-h: 56px; }
|
||
* { box-sizing:border-box; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial; }
|
||
body { margin:0; background:var(--bg); color:var(--fg); }
|
||
|
||
/* Header: 2/3 title, 1/3 actions */
|
||
header { padding:16px 20px; background:#0f172a; border-bottom:1px solid #223; position:sticky; top:0; z-index:3; }
|
||
.hdr { display:flex; align-items:center; gap:12px; }
|
||
.hdr .title { flex: 2 1 66%; font-size:20px; font-weight:600; }
|
||
.hdr .actions { flex: 1 1 34%; display:flex; justify-content:flex-end; gap:8px; }
|
||
|
||
main { max-width:1200px; margin:20px auto; padding:0 16px 40px; }
|
||
.row { display:flex; gap:16px; flex-wrap:wrap; }
|
||
.col { flex:1 1 560px; min-width:320px; }
|
||
.panel { background:var(--panel); border:1px solid #1f2937; border-radius:12px; padding:16px; }
|
||
|
||
.toolbar { margin-bottom:16px; position:sticky; top: var(--header-h); z-index:2; }
|
||
.search { width:100%; padding:10px 12px; border-radius:8px; border:1px solid #1f2937; background:#0b1220; color:var(--fg); }
|
||
|
||
.list { margin-top:12px; display:flex; flex-direction:column; gap:10px; transition: filter .2s ease; }
|
||
.card { border:1px solid #233; border-radius:10px; padding:10px 12px; background:#0c1522; cursor:default; scroll-margin-top: calc(var(--sticky-top) + 12px); }
|
||
.name { font-weight:600; display:flex; align-items:center; gap:8px; }
|
||
.meta { font-size:12px; color:var(--muted); display:flex; gap:10px; flex-wrap:wrap; margin-top:4px; }
|
||
.pill { display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; border:1px solid #2b4; }
|
||
.pill.mod { border-color:#ef4444; color:#fecaca; }
|
||
.pill.admin { border-color:#a78bfa; color:#e9d5ff; }
|
||
.pill.slash { border-color:#60a5fa; }
|
||
.pill.prefix { border-color:#f59e0b; }
|
||
.pill.hybrid { border-color:#34d399; }
|
||
|
||
/* Usage block + copy button */
|
||
.usage{
|
||
--lh:1.6; /* line-height used to size the button */
|
||
line-height:var(--lh);
|
||
position:relative;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
font-size:12px;
|
||
background:#0a1220;
|
||
border:1px dashed #1f2937;
|
||
padding:6px 40px 6px 8px; /* right padding to make room for the button */
|
||
border-radius:6px; margin-top:6px; word-break:break-word;
|
||
}
|
||
.usage > .copybtn{
|
||
position:absolute; right:0; top:0;
|
||
height: calc(var(--lh) * 1em + 12px); /* one text line + vertical padding */
|
||
width:36px;
|
||
display:flex; align-items:center; justify-content:center;
|
||
background:#374151; border:0; color:#e5e7eb; border-radius:0 6px 0 6px;
|
||
cursor:pointer;
|
||
}
|
||
.usage > .copybtn:hover{ background:#4b5563; }
|
||
@media (max-width: 900px){ .usage > .copybtn{ width:auto; padding:0 10px; } }
|
||
|
||
.help { margin-top:6px; color:#cbd5e1; }
|
||
.btn { padding:4px 8px; border:1px solid #334155; border-radius:8px; background:#0b1220; color:#e5e7eb; cursor:pointer; font-size:12px; }
|
||
.btn:hover { background:#0f172a; }
|
||
.btn-icon { width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; padding:0; }
|
||
.btn-row { display:flex; gap:8px; align-items:center; }
|
||
|
||
.detailsbox { margin-top:12px; position: sticky; top: var(--sticky-top); min-height: 280px; z-index:1; transition: transform .25s ease; }
|
||
|
||
/* Footer */
|
||
footer { margin-top:16px; color:var(--muted); font-size:12px; text-align:center; }
|
||
footer .line { margin:4px 0; }
|
||
footer a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted #334155; }
|
||
footer a:hover { text-decoration: underline; }
|
||
|
||
/* Discord drawer (right-side sheet) */
|
||
.drawer{ position:fixed; top:var(--header-h); right:0; width:clamp(360px,33vw,420px);
|
||
height:calc(100vh - var(--header-h) - 16px); background:var(--panel); border-left:1px solid #1f2937;
|
||
box-shadow:-16px 0 40px rgba(0,0,0,.45); transform:translateX(100%); transition:transform .25s ease;
|
||
z-index:6; border-top-left-radius:12px; border-bottom-left-radius:12px; overflow:hidden; }
|
||
.drawer.open{ transform:translateX(0); }
|
||
.drawer-close{ position:absolute; top:8px; right:8px; z-index:1; }
|
||
.drawer-backdrop{ position:fixed; inset:var(--header-h) 0 0 0; background:rgba(0,0,0,.35); display:none; z-index:5; }
|
||
.drawer-backdrop.open{ display:block; }
|
||
|
||
/* Blur veil under toolbar */
|
||
.veil{ position:sticky; top:var(--header-h); height:16px; pointer-events:none; z-index:2;
|
||
backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
|
||
mask-image:linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1));
|
||
-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1)); }
|
||
|
||
/* Mobile details as sheet */
|
||
@media (max-width: 900px){
|
||
.row{ display:block; }
|
||
.detailsbox{ position:fixed; right:0; top:var(--header-h); width:96vw; max-width:720px;
|
||
height:calc(100vh - var(--header-h) - 16px); overflow:auto; transform:translateX(100%);
|
||
box-shadow:-12px 0 30px rgba(0,0,0,.4); z-index:4; }
|
||
.detailsbox.open{ transform:translateX(0); }
|
||
#backdrop{ position:fixed; inset:var(--header-h) 0 0 0; background:rgba(0,0,0,.35); display:none; z-index:3; }
|
||
#backdrop.open{ display:block; }
|
||
#list.blur{ filter:blur(2px); }
|
||
#backdrop::before{ content:"‹"; position:absolute; left:10px; top:50%; transform:translateY(-50%);
|
||
font-size:48px; line-height:1; color:#e5e7eb; opacity:.8; }
|
||
}
|
||
|
||
/* Anchored copy popover + backdrop */
|
||
#copyBackdrop{ position:fixed; inset:0; display:none; z-index:20; background:transparent; }
|
||
#copyBackdrop.open{ display:block; }
|
||
#copyPopover{ position:fixed; display:none; z-index:21; background:var(--panel); border:1px solid #1f2937;
|
||
border-radius:10px; padding:10px; min-width: min(420px, 90vw);
|
||
box-shadow:0 18px 50px rgba(0,0,0,.45); }
|
||
#copyPopover.open{ display:block; }
|
||
#copyPopover .title{ font-weight:600; margin-bottom:6px; }
|
||
#copyPopover pre{ margin:6px 0; white-space:pre-wrap; word-break:break-word; background:#0a1220; border:1px dashed #1f2937; padding:8px; border-radius:6px; }
|
||
|
||
/* Tiny toast */
|
||
#toast{ position:fixed; left:50%; bottom:28px; transform:translateX(-50%); background:#111827; color:#e5e7eb;
|
||
border:1px solid #1f2937; padding:6px 10px; border-radius:999px; font-size:12px; opacity:0; pointer-events:none; z-index:40; transition:opacity .18s ease; }
|
||
#toast.show{ opacity:1; }
|
||
|
||
/* Bottom sheet (full command details) */
|
||
#fullDetailsBackdrop{
|
||
position:fixed; inset:var(--header-h) 0 0 0; display:none; z-index:12;
|
||
background:rgba(0,0,0,.35);
|
||
}
|
||
#fullDetailsBackdrop.open{ display:block; }
|
||
|
||
#fullDetailsSheet{
|
||
position:fixed; left:50%; transform:translate(-50%, 100%);
|
||
bottom:0; width:min(1200px, 94vw); max-height:75vh; overflow:auto;
|
||
background:var(--panel); border:1px solid #1f2937;
|
||
border-radius:12px 12px 0 0; box-shadow:0 18px 50px rgba(0,0,0,.45);
|
||
z-index:13; transition:transform .25s ease;
|
||
}
|
||
#fullDetailsSheet.open{ transform:translate(-50%, 0); }
|
||
|
||
#fullDetailsHead{
|
||
position:sticky; top:0; background:rgba(0,0,0,.35);
|
||
backdrop-filter: blur(6px);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
display:flex; align-items:center; gap:8px;
|
||
padding:10px 12px; border-bottom:1px solid #1f2937;
|
||
}
|
||
#fullDetailsTitle{ font-weight:600; }
|
||
#fullDetailsBody{ padding:12px; }
|
||
|
||
/* Small inline flag in footer */
|
||
.flag-emoji{
|
||
height: 1em;
|
||
width: auto;
|
||
vertical-align: -0.18em;
|
||
display: inline-block;
|
||
border-radius: 2px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="hdr">
|
||
<div class="title">__TITLE__</div>
|
||
<div class="actions">
|
||
<button id="openDiscord" class="btn btn-icon" title="Open Discord">💬</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<!-- Toolbar -->
|
||
<div class="panel toolbar" id="toolbar">
|
||
<input id="q" class="search" placeholder="Search name/description…">
|
||
<div style="margin-top:8px; font-size:12px; color:var(--muted)">
|
||
Sections: <a href="#user">User</a> · <a href="#moderator">Moderator</a> · <a href="#all">All</a>
|
||
<span id="counts" style="margin-left:10px"></span>
|
||
<span id="alerts" style="margin-left:10px; color:#fbbf24"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="veil" id="veil"></div>
|
||
|
||
<div class="row">
|
||
<div class="col"><div id="list" class="list"></div></div>
|
||
<div class="col"><div class="panel detailsbox" id="details"></div></div>
|
||
</div>
|
||
|
||
<footer id="footer">
|
||
<div class="line" id="copyright"></div>
|
||
<div class="line" id="statusline">Uptime: — · Version: v—</div>
|
||
<div class="line" id="coffee" style="display:__SUPPORT_VIS__">
|
||
<a id="supportLink" href="__SUPPORT_URL__" target="_blank" rel="noopener noreferrer">__SUPPORT_LABEL__</a>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Mobile details backdrop -->
|
||
<div id="backdrop"></div>
|
||
|
||
<!-- Discord drawer -->
|
||
<div id="discordBackdrop" class="drawer-backdrop"></div>
|
||
<div id="discordSheet" class="drawer">
|
||
<button class="btn btn-icon drawer-close" title="Close">✕</button>
|
||
<iframe id="discordWidget"
|
||
src="https://discord.com/widget?id=1396826999095427253&theme=dark"
|
||
allowtransparency="true" frameborder="0"
|
||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||
style="width:100%;height:100%;border:0"></iframe>
|
||
</div>
|
||
|
||
<!-- Anchored copy popover -->
|
||
<div id="copyBackdrop"></div>
|
||
<div id="copyPopover" role="dialog" aria-modal="true">
|
||
<div class="title">Copy to clipboard</div>
|
||
<pre id="copyText">—</pre>
|
||
<div class="btn-row">
|
||
<button id="copyAction" class="btn">Copy</button>
|
||
<button id="copyClose" class="btn">Close</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom full-details sheet -->
|
||
<div id="fullDetailsBackdrop"></div>
|
||
<div id="fullDetailsSheet" role="dialog" aria-modal="true" aria-labelledby="fullDetailsTitle">
|
||
<div id="fullDetailsHead">
|
||
<div id="fullDetailsTitle">Command details</div>
|
||
<span style="flex:1"></span>
|
||
<button id="fullDetailsClose" class="btn btn-icon" title="Close">✕</button>
|
||
</div>
|
||
<div id="fullDetailsBody"></div>
|
||
</div>
|
||
|
||
<!-- Tiny toast -->
|
||
<div id="toast" aria-live="polite">Copied!</div>
|
||
</main>
|
||
|
||
<script>
|
||
/* Sticky offsets */
|
||
function computeStickyTop(){
|
||
const header=document.querySelector('header');
|
||
const toolbar=document.getElementById('toolbar');
|
||
const headerH=header?header.offsetHeight:0;
|
||
document.documentElement.style.setProperty('--header-h', headerH+'px');
|
||
const toolbarH=toolbar?toolbar.offsetHeight:0;
|
||
const stickyTop=headerH+toolbarH+8;
|
||
document.documentElement.style.setProperty('--sticky-top', stickyTop+'px');
|
||
const veil=document.getElementById('veil');
|
||
if(veil){ veil.style.top=headerH+'px'; veil.style.height='16px'; }
|
||
}
|
||
addEventListener('resize', computeStickyTop);
|
||
addEventListener('load', computeStickyTop);
|
||
|
||
/* Error surface */
|
||
addEventListener('error', e=>{ const el=document.getElementById('alerts'); if(el) el.textContent='JS error: '+(e?.message||''); });
|
||
addEventListener('unhandledrejection', e=>{ const el=document.getElementById('alerts'); const msg=e&&(e.reason&&(e.reason.message||e.reason)||e); if(el) el.textContent='Promise error: '+msg; });
|
||
|
||
/* Mini MD (fallback when only .md exists) */
|
||
function renderMD(src){ if(!src) return ""; let s=String(src).replace(/\r\n/g,"\n");
|
||
s=s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||
s=s.replace(/```([\s\S]*?)```/g,(m,p1)=>"<pre><code>"+p1+"</code></pre>");
|
||
s=s.replace(/^###\s+(.*)$/gm,"<h3>$1</h3>");
|
||
s=s.replace(/^##\s+(.*)$/gm,"<h2>$1</h2>");
|
||
s=s.replace(/^#\s+(.*)$/gm,"<h1>$1</h1>");
|
||
s=s.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>");
|
||
s=s.replace(/\*(.+?)\*/g,"<em>$1</em>");
|
||
s=s.replace(/`([^`]+?)`/g,"<code>$1</code>");
|
||
s=s.replace(/^(\s*)-\s+(.+)$/gm,"$1<li>$2</li>");
|
||
s=s.replace(/(<li>.*<\/li>\n?)+/g,m=>"<ul>"+m+"</ul>");
|
||
s=s.replace(/^(?!<h\d|<ul>|<pre>|<li>)([^\n][^\n]*)$/gm,"<p>$1</p>");
|
||
return s; }
|
||
|
||
/* Link helpers */
|
||
function rowAnchor(r){ const cog=(r.cog||'nocog').toLowerCase();
|
||
let base=(r.name||'').toLowerCase().replace(/^\//,'').replace(/\s+/g,'-').replace(/\//g,'-'); return `${cog}-${base}`; }
|
||
function getFilterFromHash(){ const h=(location.hash||'#user').slice(1);
|
||
const parts=h.split('&').map(s=>s.trim()); const filt=parts.find(p=>p==='user'||p==='moderator'||p==='all')||'user';
|
||
let cmd=null; const cmdPart=parts.find(p=>p.startsWith('cmd=')); if(cmdPart) cmd=cmdPart.slice(4);
|
||
const sp=new URLSearchParams(location.search); const qcmd=sp.get('cmd'); if(!cmd&&qcmd) cmd=qcmd; return {filter:filt, cmd}; }
|
||
function buildLink(anchor){ const url=new URL(location.href); url.searchParams.set('cmd',anchor); url.hash=location.hash||'#user'; return url.toString(); }
|
||
function replaceURLFor(anchor){ try{ const url=new URL(location.href); url.searchParams.set('cmd',anchor); history.replaceState(null,'',url.toString()); }catch{} }
|
||
|
||
/* Copy helpers */
|
||
async function copyText(s){ try{ await navigator.clipboard.writeText(s); return true; }catch{ return false; } }
|
||
function showToast(msg='Copied!'){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'), 1100); }
|
||
|
||
const copyBackdrop=document.getElementById('copyBackdrop');
|
||
const copyPopover=document.getElementById('copyPopover');
|
||
const copyTextEl=document.getElementById('copyText');
|
||
const copyAction=document.getElementById('copyAction');
|
||
const copyClose=document.getElementById('copyClose');
|
||
|
||
function closeCopyPopover(){ copyBackdrop.classList.remove('open'); copyPopover.classList.remove('open'); }
|
||
copyBackdrop.addEventListener('click', closeCopyPopover);
|
||
copyClose.addEventListener('click', closeCopyPopover);
|
||
|
||
function openCopyPopover(text, anchorEl){
|
||
copyTextEl.textContent=text;
|
||
copyAction.onclick=async ()=>{ const ok=await copyText(text); if(ok){ closeCopyPopover(); showToast(); } };
|
||
|
||
// Position: prefer above; keep inside viewport
|
||
const r=anchorEl.getBoundingClientRect();
|
||
const pad=8, vw=innerWidth, vh=innerHeight;
|
||
copyPopover.style.visibility='hidden'; copyPopover.classList.add('open'); // measure
|
||
const pw=copyPopover.offsetWidth, ph=copyPopover.offsetHeight;
|
||
let left = Math.min(Math.max(pad, r.right - pw), vw - pw - pad);
|
||
let top = r.top - ph - pad;
|
||
if(top < (pad + 4)) top = Math.min(vh - ph - pad, r.bottom + pad);
|
||
copyPopover.style.left = `${left}px`;
|
||
copyPopover.style.top = `${top}px`;
|
||
copyPopover.style.visibility='visible';
|
||
copyBackdrop.classList.add('open');
|
||
}
|
||
|
||
/* Bottom sheet */
|
||
const fdb = document.getElementById('fullDetailsBackdrop');
|
||
const fds = document.getElementById('fullDetailsSheet');
|
||
const fdbtn = document.getElementById('fullDetailsClose');
|
||
const fdbody = document.getElementById('fullDetailsBody');
|
||
const fdtitle = document.getElementById('fullDetailsTitle');
|
||
function openFullDetails(title, html){
|
||
fdtitle.textContent = title || 'Command details';
|
||
fdbody.innerHTML = html || '<p>No extra details.</p>';
|
||
fdb.classList.add('open');
|
||
fds.classList.add('open');
|
||
}
|
||
function closeFullDetails(){ fdb.classList.remove('open'); fds.classList.remove('open'); }
|
||
fdb?.addEventListener('click', closeFullDetails);
|
||
fdbtn?.addEventListener('click', closeFullDetails);
|
||
|
||
/* App */
|
||
(function(){
|
||
if(!location.hash) location.hash='#user';
|
||
let data=(window.__DATA__||null);
|
||
|
||
const listEl=document.getElementById('list');
|
||
const qEl=document.getElementById('q');
|
||
const countsEl=document.getElementById('counts');
|
||
const detailsEl=document.getElementById('details');
|
||
const backdrop=document.getElementById('backdrop');
|
||
|
||
function shownName(r){ return (r.display_name||r.name||'').replace(/^\//,''); }
|
||
function helpSansMod(r){ return (r.help||'').replace(/^\s*\[(MOD|ADMIN)\]\s*/i,''); }
|
||
function moduleSansPrefix(r){ const m=r.module||''; return m.replace(/^modules?\./,'').replace(/^discord\.ext\./,''); }
|
||
|
||
async function shareFor(r){
|
||
const anchor=rowAnchor(r);
|
||
const url=buildLink(anchor);
|
||
const ok=await copyText(url);
|
||
if(ok) showToast('Link copied!');
|
||
else openCopyPopover(url, document.querySelector(`#card-${anchor} [data-share]`) || document.getElementById('shareDetails') || document.body);
|
||
}
|
||
|
||
function usageBlockHTML(text){
|
||
if(!text) return '';
|
||
const esc=String(text).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
return `<div class="usage">${esc}<button class="btn btn-icon copybtn" title="Copy usage" data-copy="${esc}">📋</button></div>`;
|
||
}
|
||
|
||
function wireUsageCopy(container){
|
||
container.querySelectorAll('[data-copy]').forEach(btn=>{
|
||
btn.addEventListener('click', async ev=>{
|
||
ev.stopPropagation();
|
||
const t=btn.getAttribute('data-copy').replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');
|
||
const ok=await copyText(t);
|
||
if(ok) showToast();
|
||
else openCopyPopover(t, btn);
|
||
});
|
||
});
|
||
}
|
||
|
||
function openDetails(r){
|
||
const anchor=rowAnchor(r);
|
||
replaceURLFor(anchor);
|
||
let html=`
|
||
<div class="name" style="margin-bottom:6px">
|
||
<span class="pill ${r.type}">${r.type}</span>
|
||
${r.moderator_only?'<span class="pill mod">mod</span>':''}
|
||
${r.admin_only?'<span class="pill admin">admin</span>':''}
|
||
<span>${shownName(r)}</span>
|
||
<span style="flex:1"></span>
|
||
<div class="btn-row">
|
||
<button class="btn btn-icon" title="Copy link" id="shareDetails">🔗</button>
|
||
<button class="btn" id="closeDetails" style="display:none">Close</button>
|
||
</div>
|
||
</div>
|
||
<div class="meta">
|
||
${r.cog?`<span>cog: ${r.cog}</span>`:''}
|
||
${r.module?`<span>module: ${moduleSansPrefix(r)}</span>`:''}
|
||
<span>runs: ${Number(r.exec_count||0).toLocaleString()}</span>
|
||
</div>
|
||
${usageBlockHTML(r.usage_prefix)}
|
||
${usageBlockHTML(r.usage_slash)}
|
||
${helpSansMod(r)?`<div class="help">${helpSansMod(r)}</div>`:''}
|
||
`;
|
||
|
||
// Brief/details HTML (or MD fallback)
|
||
if (r.brief_html) {
|
||
html += `<div class="md" style="margin-top:10px">${r.brief_html}</div>`;
|
||
} else if (r.details_html) {
|
||
html += `<div class="md" style="margin-top:10px">${r.details_html}</div>`;
|
||
} else if (r.details_md) {
|
||
html += `<div class="md" style="margin-top:10px">${renderMD(r.details_md)}</div>`;
|
||
} else {
|
||
html += `<div class="md" style="margin-top:10px"><p style="color:#9ca3af;font-size:14px;margin:0 0 6px 0">No custom documentation yet.</p></div>`;
|
||
}
|
||
|
||
// "Open full details" when we have long content
|
||
const hasFull = !!(r.details_html || r.details_md);
|
||
if (hasFull) {
|
||
html += `<div class="btn-row" style="margin-top:10px">
|
||
<button id="openFullDetails" class="btn">Open full details</button>
|
||
</div>`;
|
||
}
|
||
|
||
detailsEl.innerHTML=html;
|
||
|
||
const shareBtn=document.getElementById('shareDetails');
|
||
if(shareBtn) shareBtn.onclick=(e)=>{ e.stopPropagation(); shareFor(r); };
|
||
const fullBtn=document.getElementById('openFullDetails');
|
||
if(fullBtn){
|
||
fullBtn.onclick=(e)=>{
|
||
e.stopPropagation();
|
||
const longHtml = r.details_html ? r.details_html : renderMD(r.details_md || '');
|
||
openFullDetails(shownName(r), longHtml);
|
||
};
|
||
}
|
||
wireUsageCopy(detailsEl);
|
||
|
||
// Mobile sheet open
|
||
if (matchMedia('(max-width: 900px)').matches){
|
||
detailsEl.classList.add('open'); listEl.classList.add('blur'); backdrop.classList.add('open');
|
||
const closeBtn=document.getElementById('closeDetails'); if(closeBtn) closeBtn.style.display='inline-block';
|
||
backdrop.onclick=closeDetails; if(closeBtn) closeBtn.onclick=closeDetails;
|
||
}
|
||
}
|
||
function closeDetails(){ detailsEl.classList.remove('open'); listEl.classList.remove('blur'); backdrop.classList.remove('open'); }
|
||
|
||
function card(r){
|
||
const c=document.createElement('div'); const anchor=rowAnchor(r);
|
||
c.className='card'; c.id='card-'+anchor; c.dataset.anchor=anchor;
|
||
c.dataset.search=[shownName(r), helpSansMod(r), r.brief, r.usage, r.usage_prefix, r.usage_slash, r.cog, r.module, r.details_md||"", r.brief_html||"", r.details_html||""].join(' ').toLowerCase();
|
||
|
||
const usageHTML=(r.type==='hybrid') ? `${usageBlockHTML(r.usage_prefix)}${usageBlockHTML(r.usage_slash)}` : `${usageBlockHTML(r.usage)}`;
|
||
|
||
c.innerHTML=`
|
||
<div class="name">
|
||
<span class="pill ${r.type}">${r.type}</span>
|
||
${r.moderator_only?'<span class="pill mod">mod</span>':''}
|
||
${r.admin_only?'<span class="pill admin">admin</span>':''}
|
||
<span>${shownName(r)}</span>
|
||
<div class="btn-row">
|
||
<button class="btn btn-icon" title="Copy link" data-share="1">🔗</button>
|
||
<button class="btn" data-details="1">Details</button>
|
||
</div>
|
||
</div>
|
||
<div class="meta">
|
||
${r.cog?`<span>cog: ${r.cog}</span>`:''}
|
||
${r.module?`<span>module: ${moduleSansPrefix(r)}</span>`:''}
|
||
<span>runs: ${Number(r.exec_count||0).toLocaleString()}</span>
|
||
</div>
|
||
${usageHTML}
|
||
${helpSansMod(r)?`<div class="help">${helpSansMod(r)}</div>`:''}
|
||
`;
|
||
c.querySelector('[data-details]').addEventListener('click', ev=>{ ev.stopPropagation(); openDetails(r); });
|
||
c.querySelector('[data-share]').addEventListener('click', async ev=>{
|
||
ev.stopPropagation();
|
||
const url=buildLink(anchor);
|
||
const ok=await copyText(url);
|
||
if(ok) showToast('Link copied!');
|
||
else openCopyPopover(url, ev.currentTarget);
|
||
});
|
||
c.addEventListener('click', ()=>openDetails(r));
|
||
wireUsageCopy(c);
|
||
return c;
|
||
}
|
||
|
||
function render(target, rows){ target.innerHTML=''; rows.forEach(r=>target.appendChild(card(r))); }
|
||
|
||
function applyFilter(selectAnchorIfPresent=true){
|
||
if(!data) return;
|
||
const all=data.all||[], mods=(data.sections&&data.sections.moderator)||[], users=(data.sections&&data.sections.user)||[];
|
||
countsEl.textContent=`User: ${users.length} · Moderator: ${mods.length} · Total: ${all.length}`;
|
||
|
||
const q=(qEl.value||'').toLowerCase();
|
||
const {filter, cmd}=getFilterFromHash();
|
||
const src=filter==='moderator'?mods:(filter==='all'?all:users);
|
||
const rows=!q?src:src.filter(r=>([shownName(r), helpSansMod(r), r.brief, r.usage, r.usage_prefix, r.usage_slash, r.cog, r.module, r.details_md||"", r.brief_html||"", r.details_html||""].join(' ').toLowerCase().includes(q)));
|
||
render(listEl, rows);
|
||
|
||
const isMobile = matchMedia('(max-width: 900px)').matches;
|
||
|
||
// Selection logic:
|
||
// - If URL has an anchor, prefer that (scroll to it)
|
||
// - Otherwise, ONLY auto-open first row on non-mobile
|
||
let sel = null;
|
||
|
||
if (selectAnchorIfPresent && cmd) {
|
||
const hit = rows.find(r => rowAnchor(r) === cmd);
|
||
if (hit) sel = hit;
|
||
const cardEl = document.getElementById('card-'+cmd);
|
||
if (cardEl) cardEl.scrollIntoView({behavior:'smooth', block:'start'});
|
||
} else if (!isMobile && rows.length) {
|
||
sel = rows[0];
|
||
}
|
||
|
||
// Do not auto-open on mobile
|
||
if (sel && !isMobile) openDetails(sel);
|
||
}
|
||
|
||
async function boot(){
|
||
computeStickyTop();
|
||
try{
|
||
async function loadData(){
|
||
try{
|
||
const res = await fetch('/api/commands', { cache:'no-store' });
|
||
if(!res.ok) throw new Error('HTTP '+res.status);
|
||
data = await res.json();
|
||
return true; // fetched fresh
|
||
}catch(e){
|
||
// fallback to inline bootstrap if fetch fails
|
||
if(window.__DATA__){ data = window.__DATA__; return false; }
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
addEventListener('hashchange', ()=>applyFilter(false));
|
||
qEl.addEventListener('input', ()=>applyFilter(false));
|
||
|
||
await loadData(); // always refresh from backend
|
||
applyFilter(true);
|
||
}catch{ document.getElementById('alerts').textContent='Failed to load.'; }
|
||
|
||
// Footer
|
||
const fromYear=2025, now=new Date();
|
||
const flagSvg='<img class="flag-emoji" alt="NO" src="/assets/docs/no.svg">';
|
||
document.getElementById('copyright').innerHTML=`© OokamiKunTV ${fromYear}–${now.getFullYear()} — Made in ${flagSvg} with ❤️`;
|
||
try{
|
||
const s=await fetch('/api/status',{cache:'no-store'});
|
||
if(s.ok){ const js=await s.json();
|
||
document.getElementById('statusline').textContent=`Uptime: ${fmtDuration(js.uptime_seconds||0)} · Version: v${js.version||'unknown'}`; }
|
||
}catch{}
|
||
|
||
// Discord drawer
|
||
const ds=document.getElementById('discordSheet');
|
||
const db=document.getElementById('discordBackdrop');
|
||
const openBtn=document.getElementById('openDiscord');
|
||
const closeBtn=ds.querySelector('.drawer-close');
|
||
const openDiscord=()=>{ ds.classList.add('open'); db.classList.add('open'); };
|
||
const closeDiscord=()=>{ ds.classList.remove('open'); db.classList.remove('open'); };
|
||
if(openBtn) openBtn.onclick=openDiscord; if(closeBtn) closeBtn.onclick=closeDiscord; if(db) db.onclick=closeDiscord;
|
||
}
|
||
|
||
function fmtDuration(s){ s=Math.max(0,Math.floor(+s||0));
|
||
const d=Math.floor(s/86400); s%=86400; const h=Math.floor(s/3600); s%=3600; const m=Math.floor(s/60); const sec=s%60;
|
||
const parts=[]; if(d) parts.push(d+'d'); if(h||d) parts.push(h+'h'); if(m||h||d) parts.push(m+'m'); parts.push(sec+'s'); return parts.join(' '); }
|
||
|
||
setInterval(async ()=>{
|
||
try{
|
||
const gotFresh = await loadData();
|
||
if(gotFresh) applyFilter(false);
|
||
}catch{}
|
||
}, 30000);
|
||
boot();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|