1404 lines
45 KiB
HTML
1404 lines
45 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Eggdrop Config Generator</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Exo+2:wght@300;400;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #0a0d12;
|
|
--surface: #111620;
|
|
--panel: #161c28;
|
|
--border: #1e2a3a;
|
|
--accent: #00c8ff;
|
|
--accent2: #ff6b35;
|
|
--accent3: #39ff80;
|
|
--text: #c8d8e8;
|
|
--muted: #4a6070;
|
|
--comment: #3a5a40;
|
|
--comment-t: #5aaa60;
|
|
--danger: #ff4455;
|
|
--mono: 'Share Tech Mono', monospace;
|
|
--sans: 'Exo 2', sans-serif;
|
|
}
|
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--sans);
|
|
font-size: 14px;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Scanline overlay */
|
|
body::before {
|
|
content: '';
|
|
position: fixed; inset: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0,200,255,0.015) 2px,
|
|
rgba(0,200,255,0.015) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* ── HEADER ── */
|
|
header {
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
height: 64px;
|
|
position: sticky; top: 0; z-index: 100;
|
|
}
|
|
|
|
.logo {
|
|
font-family: var(--mono);
|
|
font-size: 20px;
|
|
color: var(--accent);
|
|
letter-spacing: 2px;
|
|
white-space: nowrap;
|
|
}
|
|
.logo span { color: var(--accent2); }
|
|
|
|
.header-status {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
flex: 1;
|
|
}
|
|
.header-status.ok { color: var(--accent3); }
|
|
.header-status.err { color: var(--danger); }
|
|
|
|
.btn {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
padding: 8px 18px;
|
|
border: 1px solid;
|
|
cursor: pointer;
|
|
transition: all .15s;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
background: transparent;
|
|
}
|
|
.btn-primary {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: var(--accent);
|
|
color: var(--bg);
|
|
}
|
|
.btn-success {
|
|
border-color: var(--accent3);
|
|
color: var(--accent3);
|
|
}
|
|
.btn-success:hover:not(:disabled) {
|
|
background: var(--accent3);
|
|
color: var(--bg);
|
|
}
|
|
.btn:disabled { opacity: .35; cursor: not-allowed; }
|
|
|
|
/* ── LAYOUT ── */
|
|
.shell {
|
|
display: flex;
|
|
height: calc(100vh - 64px);
|
|
}
|
|
|
|
/* ── SIDEBAR ── */
|
|
nav.sidebar {
|
|
width: 240px;
|
|
min-width: 240px;
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--border);
|
|
overflow-y: auto;
|
|
padding: 12px 0;
|
|
}
|
|
|
|
.nav-section {
|
|
padding: 6px 16px 2px;
|
|
font-family: var(--mono);
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 7px 16px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
border-left: 2px solid transparent;
|
|
transition: all .12s;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.nav-item:hover { background: var(--panel); color: var(--accent); }
|
|
.nav-item.active {
|
|
border-left-color: var(--accent);
|
|
background: var(--panel);
|
|
color: var(--accent);
|
|
}
|
|
.nav-dot {
|
|
width: 6px; height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.nav-item.active .nav-dot { background: var(--accent); }
|
|
|
|
/* ── MAIN ── */
|
|
main {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 32px;
|
|
}
|
|
|
|
/* ── LOADING/ERROR ── */
|
|
#loading, #error-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 60vh;
|
|
gap: 16px;
|
|
font-family: var(--mono);
|
|
color: var(--muted);
|
|
}
|
|
#error-panel { color: var(--danger); }
|
|
.spinner {
|
|
width: 40px; height: 40px;
|
|
border: 2px solid var(--border);
|
|
border-top-color: var(--accent);
|
|
border-radius: 50%;
|
|
animation: spin .8s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* ── SECTIONS ── */
|
|
.section-block {
|
|
display: none;
|
|
animation: fadeIn .2s ease;
|
|
}
|
|
.section-block.visible { display: block; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; } }
|
|
|
|
.section-title {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 3px;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 10px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
/* ── FIELD CARD ── */
|
|
.field-card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
margin-bottom: 16px;
|
|
transition: border-color .15s;
|
|
}
|
|
.field-card:hover { border-color: #2a3a50; }
|
|
|
|
.field-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.field-key {
|
|
font-family: var(--mono);
|
|
font-size: 13px;
|
|
color: var(--accent);
|
|
flex: 1;
|
|
}
|
|
|
|
.toggle-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
font-family: var(--mono);
|
|
}
|
|
.toggle {
|
|
position: relative;
|
|
width: 32px; height: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-slider {
|
|
position: absolute; inset: 0;
|
|
background: var(--border);
|
|
cursor: pointer;
|
|
transition: .2s;
|
|
}
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 12px; height: 12px;
|
|
left: 3px; top: 3px;
|
|
background: var(--muted);
|
|
transition: .2s;
|
|
}
|
|
.toggle input:checked + .toggle-slider { background: rgba(0,200,255,.25); }
|
|
.toggle input:checked + .toggle-slider::before {
|
|
transform: translateX(14px);
|
|
background: var(--accent);
|
|
}
|
|
|
|
.field-body { padding: 12px 14px; }
|
|
|
|
.field-comment {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--comment-t);
|
|
background: rgba(58,90,64,.15);
|
|
border-left: 2px solid var(--comment);
|
|
padding: 8px 10px;
|
|
margin-bottom: 12px;
|
|
line-height: 1.6;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.field-input {
|
|
width: 100%;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-family: var(--mono);
|
|
font-size: 13px;
|
|
padding: 8px 10px;
|
|
outline: none;
|
|
transition: border-color .15s;
|
|
}
|
|
.field-input:focus { border-color: var(--accent); }
|
|
.field-input:disabled { opacity: .4; cursor: not-allowed; }
|
|
|
|
textarea.field-input {
|
|
resize: vertical;
|
|
min-height: 80px;
|
|
}
|
|
|
|
select.field-input {
|
|
cursor: pointer;
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='%2300c8ff'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 10px center;
|
|
padding-right: 28px;
|
|
}
|
|
|
|
/* bool radio */
|
|
.bool-group {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
.bool-opt {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
cursor: pointer;
|
|
font-family: var(--mono);
|
|
font-size: 13px;
|
|
padding: 6px 14px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg);
|
|
transition: all .12s;
|
|
}
|
|
.bool-opt input { accent-color: var(--accent); }
|
|
.bool-opt:has(input:checked) {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
background: rgba(0,200,255,.07);
|
|
}
|
|
.bool-opt:has(input:disabled) { opacity: .4; cursor: not-allowed; }
|
|
|
|
/* dynamic list (servers, logfiles, channels, scripts) */
|
|
.dyn-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.dyn-item {
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
.dyn-item .field-input { flex: 1; }
|
|
.dyn-remove {
|
|
font-family: var(--mono);
|
|
font-size: 14px;
|
|
color: var(--danger);
|
|
background: none;
|
|
border: 1px solid var(--danger);
|
|
width: 28px; height: 28px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
line-height: 1;
|
|
transition: all .12s;
|
|
}
|
|
.dyn-remove:hover { background: var(--danger); color: var(--bg); }
|
|
.dyn-add {
|
|
align-self: flex-start;
|
|
margin-top: 4px;
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--accent3);
|
|
background: none;
|
|
border: 1px solid var(--accent3);
|
|
padding: 4px 12px;
|
|
cursor: pointer;
|
|
letter-spacing: 1px;
|
|
transition: all .12s;
|
|
}
|
|
.dyn-add:hover { background: var(--accent3); color: var(--bg); }
|
|
|
|
/* chanset flags grid */
|
|
.flags-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 6px;
|
|
}
|
|
.flag-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 5px 8px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg);
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: all .12s;
|
|
}
|
|
.flag-item:has(input:checked) {
|
|
border-color: var(--accent3);
|
|
color: var(--accent3);
|
|
background: rgba(57,255,128,.07);
|
|
}
|
|
.flag-item input { accent-color: var(--accent3); }
|
|
|
|
/* ── PREVIEW PANEL ── */
|
|
#preview-panel {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,.8);
|
|
z-index: 200;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
#preview-panel.open { display: flex; }
|
|
.preview-box {
|
|
width: 80vw; max-width: 900px;
|
|
height: 80vh;
|
|
background: var(--surface);
|
|
border: 1px solid var(--accent);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.preview-head {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
gap: 12px;
|
|
}
|
|
.preview-head h2 {
|
|
font-family: var(--mono);
|
|
font-size: 13px;
|
|
color: var(--accent);
|
|
flex: 1;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
}
|
|
.preview-body {
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: 16px;
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
line-height: 1.7;
|
|
color: var(--text);
|
|
white-space: pre;
|
|
}
|
|
.preview-close {
|
|
background: none;
|
|
border: 1px solid var(--muted);
|
|
color: var(--muted);
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
padding: 6px 14px;
|
|
cursor: pointer;
|
|
}
|
|
.preview-close:hover { border-color: var(--danger); color: var(--danger); }
|
|
|
|
/* scrollbar */
|
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
::-webkit-scrollbar-track { background: var(--surface); }
|
|
::-webkit-scrollbar-thumb { background: var(--border); }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="logo">EGG<span>DROP</span> CONF</div>
|
|
<div class="header-status" id="hdr-status">Fetching config from eggheads/eggdrop…</div>
|
|
<button class="btn btn-primary" id="btn-preview" disabled>Preview</button>
|
|
<button class="btn btn-success" id="btn-download" disabled>Download .conf</button>
|
|
</header>
|
|
|
|
<div class="shell">
|
|
<nav class="sidebar" id="sidebar"></nav>
|
|
<main id="main">
|
|
<div id="loading"><div class="spinner"></div><span>Loading upstream config…</span></div>
|
|
<div id="error-panel" style="display:none"></div>
|
|
<div id="sections-wrap"></div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Preview modal -->
|
|
<div id="preview-panel">
|
|
<div class="preview-box">
|
|
<div class="preview-head">
|
|
<h2>eggdrop.conf preview</h2>
|
|
<button class="btn btn-success" id="btn-dl-from-preview">Download</button>
|
|
<button class="preview-close" id="btn-close-preview">✕ Close</button>
|
|
</div>
|
|
<div class="preview-body" id="preview-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════
|
|
// PARSER — reads raw eggdrop.conf text into a schema
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
const CONF_URL = 'https://raw.githubusercontent.com/eggheads/eggdrop/refs/heads/develop/eggdrop.conf';
|
|
|
|
// Sections we treat as dynamic lists or special blocks
|
|
const DYNAMIC_KEYS = {
|
|
'server add': 'servers',
|
|
'logfile': 'logfiles',
|
|
'channel add': 'channels',
|
|
'source': 'scripts',
|
|
};
|
|
|
|
// Known boolean (0/1) keys
|
|
const BOOL_KEYS = new Set([
|
|
'prefer-ipv6','raw-log','log-time','keep-all-logs','quiet-save',
|
|
'protect-telnet','dcc-sanitycheck','require-p','open-telnets',
|
|
'stealth-telnets','use-telnet-banner','paranoid-telnet-flood',
|
|
'share-unlinks','remote-boots','force-expire','share-greet',
|
|
'use-info','allow-ps','allow-dk-cmds','cidr-support','show-uname',
|
|
'must-be-owner','keep-nick','quiet-reject','lowercase-ctcp',
|
|
'sasl','account-notify','extended-join','invite-notify',
|
|
'message-tags','account-tag','console-autosave','force-channel',
|
|
'info-party','notify-users','notify-onjoin','allow-fwd',
|
|
'upload-to-pwd','blowfish-use-mode',
|
|
]);
|
|
|
|
// Keys that are file paths
|
|
const PATH_KEYS = new Set([
|
|
'userfile','chanfile','notefile','help-path','text-path','motd',
|
|
'telnet-banner','mod-path','files-path','incoming-path','filedb-path',
|
|
'ssl-privatekey','ssl-certificate','ssl-capath','ssl-cafile',
|
|
'ssl-dhparam','sasl-ecdsa-key',
|
|
]);
|
|
|
|
// The default-chanset flags
|
|
const CHANSET_FLAGS = [
|
|
'autoop','autovoice','autohalfop','bitch','cycle','dontkickops',
|
|
'dynamicbans','dynamicexempts','dynamicinvites','enforcebans',
|
|
'greet','inactive','nodesynch','protectfriends','protecthalfops',
|
|
'protectops','revenge','revengebot','secret','seen','shared',
|
|
'static','statuslog','userbans','userexempts','userinvites',
|
|
];
|
|
|
|
function parseConf(raw) {
|
|
const lines = raw.split('\n');
|
|
const sections = [];
|
|
let currentSection = null;
|
|
let pendingComments = [];
|
|
let i = 0;
|
|
|
|
function ensureSection(name) {
|
|
if (!currentSection || currentSection.name !== name) {
|
|
currentSection = { name, fields: [], rawLines: [] };
|
|
sections.push(currentSection);
|
|
}
|
|
}
|
|
|
|
function detectSectionName(line) {
|
|
// ##### FOO BAR ##### or #### FOO BAR ####
|
|
const m = line.match(/^#+\s+([A-Z][A-Z /\-]+?)\s+#+\s*$/);
|
|
return m ? m[1].trim() : null;
|
|
}
|
|
|
|
// We'll also keep a "raw template" per section for output reconstruction
|
|
// Instead, we store rawLines on each section that we'll reconstruct at download time
|
|
|
|
ensureSection('BASIC');
|
|
|
|
while (i < lines.length) {
|
|
const line = lines[i];
|
|
|
|
// Section header?
|
|
const secName = detectSectionName(line);
|
|
if (secName) {
|
|
// flush pending comments into a raw comment field
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
ensureSection(secName);
|
|
currentSection.fields.push({ type: 'section-header', raw: line });
|
|
i++; continue;
|
|
}
|
|
|
|
// Blank line — keep as raw spacer
|
|
if (line.trim() === '') {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
currentSection.fields.push({ type: 'blank' });
|
|
i++; continue;
|
|
}
|
|
|
|
// die "..." line — mark as safety-die
|
|
if (/^die\s+"/.test(line.trim())) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
currentSection.fields.push({ type: 'die', raw: line });
|
|
i++; continue;
|
|
}
|
|
|
|
// if { ... } die line
|
|
if (/^if\s+\{.*\}\s*\{/.test(line.trim())) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
currentSection.fields.push({ type: 'die', raw: line });
|
|
i++; continue;
|
|
}
|
|
|
|
// bind evnt / proc block — collect until we see closing brace line
|
|
if (/^bind\s+evnt/.test(line.trim()) || /^proc\s+evnt:/.test(line.trim())) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
const block = [line];
|
|
i++;
|
|
while (i < lines.length) {
|
|
block.push(lines[i]);
|
|
if (lines[i].trim() === '}') { i++; break; }
|
|
i++;
|
|
}
|
|
currentSection.fields.push({ type: 'tcl-block', lines: block });
|
|
continue;
|
|
}
|
|
|
|
// unbind / addlang / loadhelp — verbatim directives
|
|
if (/^(unbind|addlang|loadhelp|pysource)\s/.test(line.trim())) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
currentSection.fields.push({ type: 'verbatim', raw: line });
|
|
i++; continue;
|
|
}
|
|
|
|
// loadmodule X
|
|
if (/^loadmodule\s+\w+/.test(line.trim())) {
|
|
const mod = line.trim().match(/^loadmodule\s+(\S+)/)[1];
|
|
currentSection.fields.push({
|
|
type: 'module',
|
|
key: 'loadmodule',
|
|
module: mod,
|
|
comment: pendingComments.join('\n'),
|
|
active: true,
|
|
raw: line,
|
|
});
|
|
pendingComments = [];
|
|
i++; continue;
|
|
}
|
|
if (/^#loadmodule\s+\w+/.test(line.trim())) {
|
|
const mod = line.trim().match(/^#loadmodule\s+(\S+)/)[1];
|
|
currentSection.fields.push({
|
|
type: 'module',
|
|
key: 'loadmodule',
|
|
module: mod,
|
|
comment: pendingComments.join('\n'),
|
|
active: false,
|
|
raw: line,
|
|
});
|
|
pendingComments = [];
|
|
i++; continue;
|
|
}
|
|
|
|
// server add ... (active or commented)
|
|
const serverMatch = line.match(/^#?\s*server\s+add\s+(.+)$/);
|
|
if (serverMatch) {
|
|
const commented = line.trimStart().startsWith('#');
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
// Find or create the servers dynamic field
|
|
let srv = currentSection.fields.find(f => f.type === 'servers');
|
|
if (!srv) {
|
|
srv = { type: 'servers', entries: [], comment: '' };
|
|
currentSection.fields.push(srv);
|
|
}
|
|
const parts = serverMatch[1].trim().split(/\s+/);
|
|
srv.entries.push({ host: parts[0], port: parts[1]||'6667', password: parts[2]||'', active: !commented });
|
|
i++; continue;
|
|
}
|
|
|
|
// logfile ... (active)
|
|
const logMatch = line.match(/^(#?)logfile\s+(\S+)\s+(\S+)\s+"([^"]+)"/);
|
|
if (logMatch) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
let lf = currentSection.fields.find(f => f.type === 'logfiles');
|
|
if (!lf) {
|
|
lf = { type: 'logfiles', entries: [], comment: '' };
|
|
currentSection.fields.push(lf);
|
|
}
|
|
lf.entries.push({
|
|
flags: logMatch[2], channel: logMatch[3], file: logMatch[4],
|
|
active: !logMatch[1],
|
|
});
|
|
i++; continue;
|
|
}
|
|
|
|
// channel add #chan
|
|
const chanMatch = line.match(/^(#?)channel\s+add\s+(\S+)/);
|
|
if (chanMatch) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
let cl = currentSection.fields.find(f => f.type === 'channels');
|
|
if (!cl) {
|
|
cl = { type: 'channels', entries: [], comment: '' };
|
|
currentSection.fields.push(cl);
|
|
}
|
|
cl.entries.push({ name: chanMatch[2], active: !chanMatch[1] });
|
|
i++; continue;
|
|
}
|
|
|
|
// source scripts/...
|
|
const srcMatch = line.match(/^(#?)source\s+(.+)$/);
|
|
if (srcMatch) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
let sl = currentSection.fields.find(f => f.type === 'scripts');
|
|
if (!sl) {
|
|
sl = { type: 'scripts', entries: [], comment: '' };
|
|
currentSection.fields.push(sl);
|
|
}
|
|
sl.entries.push({ path: srcMatch[2].trim(), active: !srcMatch[1] });
|
|
i++; continue;
|
|
}
|
|
|
|
// set default-chanset { ... } (multi-line block)
|
|
if (/^set\s+default-chanset\s+\{/.test(line.trim())) {
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
const block = [line];
|
|
let flags = {};
|
|
// parse inline
|
|
let allText = line;
|
|
if (!line.includes('}')) {
|
|
i++;
|
|
while (i < lines.length) {
|
|
block.push(lines[i]);
|
|
allText += ' ' + lines[i];
|
|
if (lines[i].includes('}')) { i++; break; }
|
|
i++;
|
|
}
|
|
} else { i++; }
|
|
// parse +/- flags
|
|
const flagRx = /([+-])(\w+)/g;
|
|
let m;
|
|
while ((m = flagRx.exec(allText)) !== null) {
|
|
flags[m[2]] = m[1] === '+';
|
|
}
|
|
currentSection.fields.push({ type: 'chanset', flags, rawBlock: block });
|
|
continue;
|
|
}
|
|
|
|
// set key "value" or set key value (active)
|
|
const setMatch = line.match(/^set\s+(\S+)\s+(.*)/);
|
|
if (setMatch) {
|
|
const key = setMatch[1];
|
|
let val = setMatch[2].trim();
|
|
// strip surrounding quotes
|
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('{') && val.endsWith('}'))) {
|
|
val = val.slice(1, -1);
|
|
}
|
|
const fieldType = inferType(key, val);
|
|
currentSection.fields.push({
|
|
type: 'setting',
|
|
key,
|
|
value: val,
|
|
active: true,
|
|
comment: pendingComments.join('\n'),
|
|
fieldType,
|
|
});
|
|
pendingComments = [];
|
|
i++; continue;
|
|
}
|
|
|
|
// #set key ... (commented-out setting)
|
|
const csetMatch = line.match(/^#set\s+(\S+)\s+(.*)/);
|
|
if (csetMatch) {
|
|
const key = csetMatch[1];
|
|
let val = csetMatch[2].trim();
|
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('{') && val.endsWith('}'))) {
|
|
val = val.slice(1, -1);
|
|
}
|
|
const fieldType = inferType(key, val);
|
|
currentSection.fields.push({
|
|
type: 'setting',
|
|
key,
|
|
value: val,
|
|
active: false,
|
|
comment: pendingComments.join('\n'),
|
|
fieldType,
|
|
});
|
|
pendingComments = [];
|
|
i++; continue;
|
|
}
|
|
|
|
// Comment line — accumulate
|
|
if (line.trim().startsWith('#')) {
|
|
pendingComments.push(line);
|
|
i++; continue;
|
|
}
|
|
|
|
// Anything else — verbatim
|
|
if (pendingComments.length) {
|
|
currentSection.fields.push({ type: 'raw-comment', lines: [...pendingComments] });
|
|
pendingComments = [];
|
|
}
|
|
currentSection.fields.push({ type: 'verbatim', raw: line });
|
|
i++;
|
|
}
|
|
|
|
if (pendingComments.length) {
|
|
if (currentSection) currentSection.fields.push({ type: 'raw-comment', lines: pendingComments });
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
function inferType(key, val) {
|
|
if (BOOL_KEYS.has(key)) return 'bool';
|
|
if (PATH_KEYS.has(key)) return 'path';
|
|
if (/^\d+$/.test(val) && !key.includes('format') && !key.includes('suffix')) return 'int';
|
|
// flood rates like 3:10
|
|
if (/^\d+:\d+$/.test(val)) return 'rate';
|
|
return 'string';
|
|
}
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// STATE
|
|
// ═══════════════════════════════════════════════════════
|
|
let SCHEMA = []; // parsed sections
|
|
let DOM_IDS = {}; // key => element id for reading values
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// RENDER UI
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function renderUI(sections) {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const wrap = document.getElementById('sections-wrap');
|
|
sidebar.innerHTML = '';
|
|
wrap.innerHTML = '';
|
|
|
|
sections.forEach((sec, si) => {
|
|
// Only add sections that have something interactive
|
|
const hasInteractive = sec.fields.some(f =>
|
|
['setting','module','servers','logfiles','channels','scripts','chanset'].includes(f.type)
|
|
);
|
|
if (!hasInteractive) return;
|
|
|
|
// Sidebar item
|
|
const navItem = document.createElement('div');
|
|
navItem.className = 'nav-item' + (si === 0 ? ' active' : '');
|
|
navItem.dataset.secid = si;
|
|
navItem.innerHTML = `<span class="nav-dot"></span>${sec.name}`;
|
|
navItem.addEventListener('click', () => showSection(si));
|
|
sidebar.appendChild(navItem);
|
|
|
|
// Section block
|
|
const block = document.createElement('div');
|
|
block.className = 'section-block' + (si === 0 ? ' visible' : '');
|
|
block.id = `sec-${si}`;
|
|
block.innerHTML = `<div class="section-title">// ${sec.name}</div>`;
|
|
|
|
sec.fields.forEach((field, fi) => {
|
|
const el = renderField(field, si, fi);
|
|
if (el) block.appendChild(el);
|
|
});
|
|
|
|
wrap.appendChild(block);
|
|
});
|
|
}
|
|
|
|
let _uid = 0;
|
|
function uid() { return 'f' + (_uid++); }
|
|
|
|
function renderField(field, si, fi) {
|
|
switch (field.type) {
|
|
case 'setting': return renderSetting(field, si, fi);
|
|
case 'module': return renderModule(field, si, fi);
|
|
case 'servers': return renderServers(field, si, fi);
|
|
case 'logfiles':return renderLogfiles(field, si, fi);
|
|
case 'channels':return renderChannels(field, si, fi);
|
|
case 'scripts': return renderScripts(field, si, fi);
|
|
case 'chanset': return renderChanset(field, si, fi);
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
function commentText(raw) {
|
|
return raw.split('\n')
|
|
.map(l => l.replace(/^#+\s?/, ''))
|
|
.join('\n')
|
|
.trim();
|
|
}
|
|
|
|
function renderSetting(field, si, fi) {
|
|
const id = uid();
|
|
field._id = id;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
|
|
const toggleId = uid();
|
|
card.innerHTML = `
|
|
<div class="field-header">
|
|
<span class="field-key">set ${field.key}</span>
|
|
<label class="toggle-wrap">
|
|
<label class="toggle">
|
|
<input type="checkbox" id="tog-${id}" ${field.active ? 'checked' : ''}>
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<span>enabled</span>
|
|
</label>
|
|
</div>
|
|
<div class="field-body" id="body-${id}">
|
|
${field.comment ? `<div class="field-comment">${escHtml(commentText(field.comment))}</div>` : ''}
|
|
${renderInput(field, id)}
|
|
</div>
|
|
`;
|
|
|
|
// wire toggle
|
|
const tog = card.querySelector(`#tog-${id}`);
|
|
const body = card.querySelector(`#body-${id}`);
|
|
function applyEnabled(en) {
|
|
body.querySelectorAll('input:not([type=checkbox]),textarea,select').forEach(el => el.disabled = !en);
|
|
body.querySelectorAll('.bool-opt').forEach(el => el.style.opacity = en ? '' : '.4');
|
|
}
|
|
applyEnabled(field.active);
|
|
tog.addEventListener('change', () => { field.active = tog.checked; applyEnabled(tog.checked); });
|
|
|
|
return card;
|
|
}
|
|
|
|
function renderInput(field, id) {
|
|
const v = escHtml(field.value);
|
|
if (field.fieldType === 'bool') {
|
|
return `<div class="bool-group">
|
|
<label class="bool-opt"><input type="radio" name="${id}" value="1" ${field.value==='1'?'checked':''}> 1 (on)</label>
|
|
<label class="bool-opt"><input type="radio" name="${id}" value="0" ${field.value!=='1'?'checked':''}> 0 (off)</label>
|
|
</div>`;
|
|
}
|
|
if (field.key === 'net-type') {
|
|
const nets = ['EFnet','IRCnet','Undernet','DALnet','Libera','freenode','QuakeNet','Rizon','Twitch','Other'];
|
|
return `<select class="field-input" id="inp-${id}">` +
|
|
nets.map(n => `<option ${field.value===n?'selected':''}>${n}</option>`).join('') +
|
|
'</select>';
|
|
}
|
|
if (field.key === 'blowfish-use-mode') {
|
|
return `<div class="bool-group">
|
|
<label class="bool-opt"><input type="radio" name="${id}" value="cbc" ${field.value==='cbc'?'checked':''}> cbc</label>
|
|
<label class="bool-opt"><input type="radio" name="${id}" value="ecb" ${field.value!=='cbc'?'checked':''}> ecb</label>
|
|
</div>`;
|
|
}
|
|
if (field.key === 'console') {
|
|
return `<input class="field-input" id="inp-${id}" value="${v}" placeholder="mkcoblxs" style="width:200px">`;
|
|
}
|
|
if (field.key === 'default-chanset') return ''; // handled separately
|
|
if (field.key.endsWith('-flood') || field.key.startsWith('default-flood') || field.key === 'telnet-flood' || field.key === 'default-aop-delay') {
|
|
return `<input class="field-input" id="inp-${id}" value="${v}" placeholder="N:N" style="width:160px">`;
|
|
}
|
|
if (field.fieldType === 'int') {
|
|
return `<input class="field-input" type="number" id="inp-${id}" value="${v}" style="width:160px">`;
|
|
}
|
|
// long string
|
|
if (field.value && field.value.length > 60) {
|
|
return `<textarea class="field-input" id="inp-${id}">${v}</textarea>`;
|
|
}
|
|
return `<input class="field-input" id="inp-${id}" value="${v}">`;
|
|
}
|
|
|
|
function renderModule(field, si, fi) {
|
|
const id = uid();
|
|
field._id = id;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
card.innerHTML = `
|
|
<div class="field-header">
|
|
<span class="field-key">loadmodule ${field.module}</span>
|
|
<label class="toggle-wrap">
|
|
<label class="toggle">
|
|
<input type="checkbox" id="tog-${id}" ${field.active ? 'checked' : ''}>
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<span>load</span>
|
|
</label>
|
|
</div>
|
|
${field.comment ? `<div class="field-body"><div class="field-comment">${escHtml(commentText(field.comment))}</div></div>` : ''}
|
|
`;
|
|
const tog = card.querySelector(`#tog-${id}`);
|
|
tog.addEventListener('change', () => { field.active = tog.checked; });
|
|
return card;
|
|
}
|
|
|
|
function renderServers(field, si, fi) {
|
|
field._id = 'servers-' + si;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
card.innerHTML = `
|
|
<div class="field-header"><span class="field-key">server add <span style="color:var(--muted);font-size:11px">— IRC server list</span></span></div>
|
|
<div class="field-body">
|
|
<div class="field-comment">Add servers to connect to. Prefix port with + for SSL. Password is optional.</div>
|
|
<div class="dyn-list" id="srv-list-${si}"></div>
|
|
<button class="dyn-add" id="srv-add-${si}">+ Add server</button>
|
|
</div>`;
|
|
const list = card.querySelector(`#srv-list-${si}`);
|
|
|
|
function addRow(entry) {
|
|
const row = document.createElement('div');
|
|
row.className = 'dyn-item';
|
|
row.innerHTML = `
|
|
<input class="field-input" placeholder="server.host.name" value="${escHtml(entry.host)}" style="flex:3" data-role="host">
|
|
<input class="field-input" placeholder="port" value="${escHtml(entry.port)}" style="flex:1;max-width:90px" data-role="port">
|
|
<input class="field-input" placeholder="password (opt)" value="${escHtml(entry.password)}" style="flex:2" data-role="pass">
|
|
<button class="dyn-remove" title="Remove">✕</button>`;
|
|
row.querySelector('.dyn-remove').addEventListener('click', () => row.remove());
|
|
list.appendChild(row);
|
|
}
|
|
|
|
field.entries.forEach(e => addRow(e));
|
|
card.querySelector(`#srv-add-${si}`).addEventListener('click', () =>
|
|
addRow({ host: '', port: '6667', password: '' })
|
|
);
|
|
return card;
|
|
}
|
|
|
|
function renderLogfiles(field, si, fi) {
|
|
field._id = 'logfiles-' + si;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
card.innerHTML = `
|
|
<div class="field-header"><span class="field-key">logfile <span style="color:var(--muted);font-size:11px">— log targets</span></span></div>
|
|
<div class="field-body">
|
|
<div class="field-comment">Flags: b=botlink c=commands d=debug j=joins k=kicks m=msgs o=misc p=public r=raw-in s=server v=raw-out</div>
|
|
<div class="dyn-list" id="lf-list-${si}"></div>
|
|
<button class="dyn-add" id="lf-add-${si}">+ Add logfile</button>
|
|
</div>`;
|
|
const list = card.querySelector(`#lf-list-${si}`);
|
|
function addRow(e) {
|
|
const row = document.createElement('div');
|
|
row.className = 'dyn-item';
|
|
row.innerHTML = `
|
|
<input class="field-input" placeholder="flags e.g. mco" value="${escHtml(e.flags)}" style="flex:1;max-width:90px" data-role="flags">
|
|
<input class="field-input" placeholder="channel (*=all)" value="${escHtml(e.channel)}" style="flex:1;max-width:120px" data-role="channel">
|
|
<input class="field-input" placeholder="logs/file.log" value="${escHtml(e.file)}" style="flex:3" data-role="file">
|
|
<button class="dyn-remove">✕</button>`;
|
|
row.querySelector('.dyn-remove').addEventListener('click', () => row.remove());
|
|
list.appendChild(row);
|
|
}
|
|
field.entries.forEach(e => addRow(e));
|
|
card.querySelector(`#lf-add-${si}`).addEventListener('click', () =>
|
|
addRow({ flags: 'mco', channel: '*', file: 'logs/eggdrop.log' })
|
|
);
|
|
return card;
|
|
}
|
|
|
|
function renderChannels(field, si, fi) {
|
|
field._id = 'channels-' + si;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
card.innerHTML = `
|
|
<div class="field-header"><span class="field-key">channel add <span style="color:var(--muted);font-size:11px">— channels to join</span></span></div>
|
|
<div class="field-body">
|
|
<div class="field-comment">Channels to join on startup. You can also add channels via partyline with .+chan</div>
|
|
<div class="dyn-list" id="ch-list-${si}"></div>
|
|
<button class="dyn-add" id="ch-add-${si}">+ Add channel</button>
|
|
</div>`;
|
|
const list = card.querySelector(`#ch-list-${si}`);
|
|
function addRow(e) {
|
|
const row = document.createElement('div');
|
|
row.className = 'dyn-item';
|
|
row.innerHTML = `
|
|
<input class="field-input" placeholder="#channel" value="${escHtml(e.name)}">
|
|
<button class="dyn-remove">✕</button>`;
|
|
row.querySelector('.dyn-remove').addEventListener('click', () => row.remove());
|
|
list.appendChild(row);
|
|
}
|
|
field.entries.forEach(e => addRow(e));
|
|
card.querySelector(`#ch-add-${si}`).addEventListener('click', () =>
|
|
addRow({ name: '#channel' })
|
|
);
|
|
return card;
|
|
}
|
|
|
|
function renderScripts(field, si, fi) {
|
|
field._id = 'scripts-' + si;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
card.innerHTML = `
|
|
<div class="field-header"><span class="field-key">source <span style="color:var(--muted);font-size:11px">— Tcl scripts to load</span></span></div>
|
|
<div class="field-body">
|
|
<div class="dyn-list" id="sc-list-${si}"></div>
|
|
<button class="dyn-add" id="sc-add-${si}">+ Add script</button>
|
|
</div>`;
|
|
const list = card.querySelector(`#sc-list-${si}`);
|
|
function addRow(e) {
|
|
const row = document.createElement('div');
|
|
row.className = 'dyn-item';
|
|
const chkId = uid();
|
|
row.innerHTML = `
|
|
<label class="toggle-wrap" style="flex-shrink:0">
|
|
<label class="toggle"><input type="checkbox" id="${chkId}" ${e.active?'checked':''}><span class="toggle-slider"></span></label>
|
|
<span style="font-size:11px">load</span>
|
|
</label>
|
|
<input class="field-input" placeholder="scripts/myscript.tcl" value="${escHtml(e.path)}">
|
|
<button class="dyn-remove">✕</button>`;
|
|
row.querySelector('.dyn-remove').addEventListener('click', () => row.remove());
|
|
list.appendChild(row);
|
|
}
|
|
field.entries.forEach(e => addRow(e));
|
|
card.querySelector(`#sc-add-${si}`).addEventListener('click', () =>
|
|
addRow({ path: 'scripts/', active: true })
|
|
);
|
|
return card;
|
|
}
|
|
|
|
function renderChanset(field, si, fi) {
|
|
field._id = 'chanset-' + si;
|
|
const card = document.createElement('div');
|
|
card.className = 'field-card';
|
|
const flagsHtml = CHANSET_FLAGS.map(f => `
|
|
<label class="flag-item">
|
|
<input type="checkbox" name="cs-${f}" ${field.flags[f] ? 'checked' : ''}> ${f}
|
|
</label>`).join('');
|
|
card.innerHTML = `
|
|
<div class="field-header"><span class="field-key">set default-chanset <span style="color:var(--muted);font-size:11px">— default channel flags</span></span></div>
|
|
<div class="field-body">
|
|
<div class="field-comment">These flags apply to newly added channels by default.</div>
|
|
<div class="flags-grid" id="cs-grid-${si}">${flagsHtml}</div>
|
|
</div>`;
|
|
return card;
|
|
}
|
|
|
|
function showSection(si) {
|
|
document.querySelectorAll('.section-block').forEach(b => b.classList.remove('visible'));
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
const block = document.getElementById(`sec-${si}`);
|
|
if (block) block.classList.add('visible');
|
|
document.querySelectorAll(`.nav-item[data-secid="${si}"]`).forEach(n => n.classList.add('active'));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// GENERATOR — reads DOM back into .conf text
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function generateConf(sections) {
|
|
const out = [];
|
|
|
|
sections.forEach(sec => {
|
|
sec.fields.forEach(field => {
|
|
switch (field.type) {
|
|
case 'blank':
|
|
out.push('');
|
|
break;
|
|
|
|
case 'raw-comment':
|
|
field.lines.forEach(l => out.push(l));
|
|
break;
|
|
|
|
case 'section-header':
|
|
out.push(field.raw);
|
|
break;
|
|
|
|
case 'die':
|
|
// Replace die lines with a comment
|
|
out.push(`# [config generator] die line removed — config is ready to use`);
|
|
break;
|
|
|
|
case 'verbatim':
|
|
out.push(field.raw);
|
|
break;
|
|
|
|
case 'tcl-block':
|
|
field.lines.forEach(l => out.push(l));
|
|
break;
|
|
|
|
case 'setting': {
|
|
// Write comment lines
|
|
if (field.comment) field.comment.split('\n').forEach(l => out.push(l));
|
|
const val = readSettingValue(field);
|
|
const prefix = field.active ? '' : '#';
|
|
if (field.fieldType === 'bool' || field.fieldType === 'int' || field.fieldType === 'rate') {
|
|
out.push(`${prefix}set ${field.key} ${val}`);
|
|
} else if (val.includes('\n') || (val.includes(' ') && !val.startsWith('$'))) {
|
|
out.push(`${prefix}set ${field.key} "${val}"`);
|
|
} else {
|
|
out.push(`${prefix}set ${field.key} "${val}"`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'module': {
|
|
if (field.comment) field.comment.split('\n').forEach(l => out.push(l));
|
|
out.push(`${field.active ? '' : '#'}loadmodule ${field.module}`);
|
|
break;
|
|
}
|
|
|
|
case 'servers': {
|
|
const rows = getServerRows(field._id.replace('servers-', ''));
|
|
if (rows.length === 0) {
|
|
out.push(`# server add <host> <port> [password]`);
|
|
} else {
|
|
rows.forEach(r => out.push(`server add ${r.host} ${r.port}${r.password ? ' ' + r.password : ''}`));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'logfiles': {
|
|
const rows = getLogRows(field._id.replace('logfiles-', ''));
|
|
rows.forEach(r => out.push(`logfile ${r.flags} ${r.channel} "${r.file}"`));
|
|
break;
|
|
}
|
|
|
|
case 'channels': {
|
|
const rows = getChanRows(field._id.replace('channels-', ''));
|
|
rows.forEach(r => out.push(`channel add ${r}`));
|
|
break;
|
|
}
|
|
|
|
case 'scripts': {
|
|
const rows = getScriptRows(field._id.replace('scripts-', ''));
|
|
rows.forEach(r => out.push(`${r.active ? '' : '#'}source ${r.path}`));
|
|
break;
|
|
}
|
|
|
|
case 'chanset': {
|
|
const flags = readChansetFlags(field._id.replace('chanset-', ''));
|
|
out.push('set default-chanset {');
|
|
const names = Object.keys(flags);
|
|
for (let i = 0; i < names.length; i += 2) {
|
|
const a = names[i], va = flags[a] ? '+' : '-';
|
|
const b = names[i+1], vb = b ? (flags[b] ? '+' : '-') : null;
|
|
if (vb !== null) {
|
|
out.push(`\t${va}${a.padEnd(20)} ${vb}${b}`);
|
|
} else {
|
|
out.push(`\t${va}${a}`);
|
|
}
|
|
}
|
|
out.push('}');
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
return out.join('\n');
|
|
}
|
|
|
|
// ── value readers ──
|
|
|
|
function readSettingValue(field) {
|
|
if (field.fieldType === 'bool' || field.key === 'blowfish-use-mode') {
|
|
const checked = document.querySelector(`input[name="f${field._id.slice(1)}"]:checked`)
|
|
|| document.querySelector(`input[name="${field._id}"]:checked`);
|
|
// fallback: find radio by scanning
|
|
const radios = document.querySelectorAll(`input[name="${field._id}"]`);
|
|
if (radios.length) {
|
|
for (const r of radios) { if (r.checked) return r.value; }
|
|
}
|
|
return field.value;
|
|
}
|
|
const el = document.getElementById(`inp-${field._id}`);
|
|
return el ? el.value : field.value;
|
|
}
|
|
|
|
function getServerRows(si) {
|
|
const list = document.getElementById(`srv-list-${si}`);
|
|
if (!list) return [];
|
|
return Array.from(list.querySelectorAll('.dyn-item')).map(row => ({
|
|
host: row.querySelector('[data-role=host]').value.trim(),
|
|
port: row.querySelector('[data-role=port]').value.trim(),
|
|
password: row.querySelector('[data-role=pass]').value.trim(),
|
|
})).filter(r => r.host);
|
|
}
|
|
|
|
function getLogRows(si) {
|
|
const list = document.getElementById(`lf-list-${si}`);
|
|
if (!list) return [];
|
|
return Array.from(list.querySelectorAll('.dyn-item')).map(row => ({
|
|
flags: row.querySelector('[data-role=flags]').value.trim(),
|
|
channel: row.querySelector('[data-role=channel]').value.trim(),
|
|
file: row.querySelector('[data-role=file]').value.trim(),
|
|
})).filter(r => r.file);
|
|
}
|
|
|
|
function getChanRows(si) {
|
|
const list = document.getElementById(`ch-list-${si}`);
|
|
if (!list) return [];
|
|
return Array.from(list.querySelectorAll('.dyn-item input')).map(i => i.value.trim()).filter(Boolean);
|
|
}
|
|
|
|
function getScriptRows(si) {
|
|
const list = document.getElementById(`sc-list-${si}`);
|
|
if (!list) return [];
|
|
return Array.from(list.querySelectorAll('.dyn-item')).map(row => ({
|
|
active: row.querySelector('input[type=checkbox]').checked,
|
|
path: row.querySelector('input[type=text], input:not([type=checkbox])').value.trim(),
|
|
})).filter(r => r.path);
|
|
}
|
|
|
|
function readChansetFlags(si) {
|
|
const grid = document.getElementById(`cs-grid-${si}`);
|
|
const result = {};
|
|
if (!grid) {
|
|
CHANSET_FLAGS.forEach(f => result[f] = false);
|
|
return result;
|
|
}
|
|
CHANSET_FLAGS.forEach(f => {
|
|
const cb = grid.querySelector(`input[name="cs-${f}"]`);
|
|
result[f] = cb ? cb.checked : false;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// ── helpers ──
|
|
function escHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g,'&')
|
|
.replace(/</g,'<')
|
|
.replace(/>/g,'>')
|
|
.replace(/"/g,'"');
|
|
}
|
|
|
|
function download(text, filename) {
|
|
const blob = new Blob([text], { type: 'text/plain' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = filename;
|
|
a.click();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// BOOTSTRAP
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
async function init() {
|
|
const status = document.getElementById('hdr-status');
|
|
try {
|
|
const resp = await fetch(CONF_URL);
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
const raw = await resp.text();
|
|
|
|
SCHEMA = parseConf(raw);
|
|
|
|
document.getElementById('loading').style.display = 'none';
|
|
renderUI(SCHEMA);
|
|
|
|
// show first visible section
|
|
const first = document.querySelector('.nav-item');
|
|
if (first) showSection(parseInt(first.dataset.secid));
|
|
|
|
status.textContent = `✓ Loaded ${SCHEMA.length} sections from eggheads/eggdrop`;
|
|
status.className = 'header-status ok';
|
|
|
|
document.getElementById('btn-preview').disabled = false;
|
|
document.getElementById('btn-download').disabled = false;
|
|
|
|
} catch(e) {
|
|
document.getElementById('loading').style.display = 'none';
|
|
const ep = document.getElementById('error-panel');
|
|
ep.style.display = 'flex';
|
|
ep.innerHTML = `<div>Failed to fetch config</div><div style="font-size:11px">${e.message}</div>
|
|
<div style="font-size:11px;color:var(--muted)">Check CORS — you may need to open this file via a local server</div>`;
|
|
status.textContent = '✗ Fetch failed';
|
|
status.className = 'header-status err';
|
|
}
|
|
}
|
|
|
|
document.getElementById('btn-preview').addEventListener('click', () => {
|
|
const text = generateConf(SCHEMA);
|
|
document.getElementById('preview-body').textContent = text;
|
|
document.getElementById('preview-panel').classList.add('open');
|
|
});
|
|
|
|
document.getElementById('btn-close-preview').addEventListener('click', () =>
|
|
document.getElementById('preview-panel').classList.remove('open')
|
|
);
|
|
|
|
document.getElementById('preview-panel').addEventListener('click', e => {
|
|
if (e.target === document.getElementById('preview-panel'))
|
|
document.getElementById('preview-panel').classList.remove('open');
|
|
});
|
|
|
|
document.getElementById('btn-download').addEventListener('click', () =>
|
|
download(generateConf(SCHEMA), 'eggdrop.conf')
|
|
);
|
|
document.getElementById('btn-dl-from-preview').addEventListener('click', () =>
|
|
download(generateConf(SCHEMA), 'eggdrop.conf')
|
|
);
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|