You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1405 lines
82 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>TD-Pult</title>
<style>
:root{
--bg:#f4f6f9; --panel:#ffffff; --panel2:#eef1f6; --panel3:#e5eaf1; --line:#d7dde6;
--tx:#131922; --tx2:#4d5967; --tx3:#8a95a4;
--acc:#0b64d0; --accbg:#e4eefc;
--ok:#0f7a4d; --okbg:#e0f4ea;
--warn:#9a5b00; --warnbg:#fdf0dc;
--crit:#c22318; --critbg:#fdeae8;
--vio:#5f37c9; --viobg:#efe9fd;
--r:10px;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{
background:var(--bg); color:var(--tx); font:15px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
-webkit-font-smoothing:antialiased; overflow:hidden; display:flex; flex-direction:column;
}
button,input,textarea,select{font:inherit;color:inherit}
button{cursor:pointer;background:none;border:none}
input,select,textarea{background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:7px 9px}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--acc);box-shadow:0 0 0 3px var(--accbg)}
/* ---------- Kopf ---------- */
header{display:flex;align-items:center;gap:12px;padding:8px 14px;flex:none;background:var(--panel);border-bottom:1px solid var(--line)}
.clock{font-variant-numeric:tabular-nums;font-size:56px;font-weight:650;line-height:1;letter-spacing:-.01em}
.clock small{display:block;font-size:11px;color:var(--tx3);letter-spacing:.14em;font-weight:700;margin-top:4px}
.tabs{display:flex;gap:4px;background:var(--panel2);padding:3px;border-radius:var(--r)}
.tabs button{padding:6px 12px;border-radius:7px;font-size:13px;color:var(--tx2);font-weight:500}
.tabs button.on{background:var(--acc);color:#fff;font-weight:700}
.spacer{flex:1}
.hbtn{padding:7px 12px;border:1px solid var(--line);border-radius:8px;font-size:13px;color:var(--tx2);background:var(--panel)}
.hbtn:hover{border-color:var(--tx3);color:var(--tx)}
.hbtn.on{background:var(--ok);color:#fff;border-color:transparent;font-weight:700}
.hbtn.edit{background:var(--warn);color:#fff;border-color:transparent;font-weight:700}
.hbtn.bau{background:var(--vio);color:#fff;border-color:transparent;font-weight:700}
.motto{font-size:12px;color:var(--tx3);max-width:300px;line-height:1.3}
.ttl{display:flex;flex-direction:column;line-height:1.25;font-size:12px;color:var(--tx3)}
.ttl b{font-size:14px;color:var(--tx);font-weight:650}
/* ---------- Layout ---------- */
#app{display:flex;flex:1;min-height:0}
#side{width:262px;flex:none;border-right:1px solid var(--line);background:var(--panel);overflow-y:auto;padding-bottom:30px}
#main{flex:1;min-width:0;overflow-y:auto;padding:14px 16px 70px}
#right{width:44%;max-width:760px;min-width:340px;flex:none;border-left:1px solid var(--line);background:var(--panel);display:flex;flex-direction:column}
#rhead{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--line);flex:none}
#rbody{overflow-y:auto;padding:14px 16px 70px;flex:1}
/* ---------- Ablaufliste ---------- */
.sh{padding:9px 12px 5px;font-size:10px;letter-spacing:.14em;color:var(--tx3);font-weight:700}
.li{display:flex;gap:8px;padding:8px 8px 8px 12px;border-left:3px solid transparent;cursor:pointer;align-items:flex-start;border-bottom:1px solid var(--panel2)}
.li:hover{background:var(--panel2)}
.li.on{background:var(--accbg);border-left-color:var(--acc)}
.li.done{opacity:.5}
.li .t{font-variant-numeric:tabular-nums;font-size:12px;color:var(--tx3);width:38px;flex:none;padding-top:2px;font-weight:600}
.li.on .t{color:var(--acc)}
.li .n{font-size:13px;line-height:1.3;font-weight:650}
.li .f{font-size:11.5px;color:var(--tx2);margin-top:2px;line-height:1.35}
.li .m{font-size:10px;color:var(--tx3);margin-top:3px;display:flex;gap:5px;flex-wrap:wrap;letter-spacing:.05em;font-weight:700}
.li .full{font-size:11.5px;color:var(--tx2);margin-top:5px;line-height:1.45;border-top:1px solid var(--line);padding-top:5px}
.li .full p{margin:0 0 5px}
.li .exp{color:var(--tx3);font-size:13px;padding:0 2px;flex:none;line-height:1.2}
.li .exp:hover{color:var(--acc)}
.li .tools{display:flex;gap:2px;margin-top:5px}
.li .tools button{font-size:11px;padding:2px 6px;border:1px solid var(--line);border-radius:5px;color:var(--tx2);background:var(--panel)}
.li .tools button:hover{border-color:var(--acc);color:var(--acc)}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-top:6px;flex:none}
/* ---------- Karten ---------- */
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);margin-bottom:12px}
.card>.hd{display:flex;align-items:center;gap:9px;padding:9px 13px;border-bottom:1px solid var(--line)}
.card>.hd h3{margin:0;font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700}
.card>.bd{padding:13px}
/* ---------- Jetzt ---------- */
.now{background:linear-gradient(180deg,var(--accbg),var(--panel));border:1px solid #b9d3f4;border-radius:var(--r);padding:15px 17px;margin-bottom:12px}
.now .kick{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:7px}
.now .time{font-size:26px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1;color:var(--acc)}
.now h1{margin:0 0 5px;font-size:24px;line-height:1.2;font-weight:680;letter-spacing:-.01em}
.now .sub{font-size:13.5px;color:var(--tx2)}
.now .facts{font-size:14px;margin-top:6px;font-weight:600}
.badge{font-size:10px;font-weight:700;letter-spacing:.09em;padding:3px 7px;border-radius:5px;background:var(--panel2);color:var(--tx2);border:1px solid var(--line)}
.badge.sync{background:var(--viobg);color:var(--vio);border-color:#cbb8f5}
.badge.stream{background:var(--warnbg);color:var(--warn);border-color:#eccb95}
.badge.art{background:var(--accbg);color:var(--acc);border-color:#b9d3f4}
/* ---------- Nächstes Video ---------- */
.nv{border:1px solid #cbb8f5;background:linear-gradient(180deg,var(--viobg),var(--panel));border-radius:var(--r);padding:13px 15px;margin-bottom:12px}
.nv .lbl{font-size:10px;letter-spacing:.14em;font-weight:700;color:var(--vio);margin-bottom:7px}
.nv .line{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
.nv .id{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:26px;font-weight:700}
.nv .eta{font-size:20px;font-weight:700;color:var(--vio);font-variant-numeric:tabular-nums}
.nv .meta{font-size:13px;color:var(--tx2);margin-top:5px}
.nv .acts{display:flex;gap:7px;margin-top:9px;flex-wrap:wrap;align-items:center}
.nv .steps{display:flex;align-items:center;gap:0;margin-top:11px;flex-wrap:wrap}
.nv .stp{display:flex;align-items:center;gap:7px;padding:9px 14px;border:1px solid #cbb8f5;background:var(--panel);
color:var(--tx2);font-size:13.5px;font-weight:650;border-radius:9px}
.nv .stp b{display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;border-radius:50%;
background:var(--panel2);color:var(--tx3);font-size:11px;font-weight:800}
.nv .stp:hover{border-color:var(--vio);color:var(--vio)}
.nv .stp.on{background:var(--okbg);border-color:var(--ok);color:var(--ok)}
.nv .stp.on b{background:var(--ok);color:#fff}
.nv .stp.go{background:var(--vio);border-color:var(--vio);color:#fff}
.nv .stp.go b{background:rgba(255,255,255,.25);color:#fff}
.nv .stp.go:hover{background:#4f2cab;color:#fff}
.nv .sep{width:16px;height:1px;background:#cbb8f5;flex:none}
.nv .abtn{padding:7px 12px;border-radius:8px;font-size:13px;font-weight:650;border:1px solid #cbb8f5;color:var(--vio);background:var(--panel)}
.nv .abtn:hover{background:var(--viobg)}
.nv .abtn.ok{border-color:var(--ok);color:#fff;background:var(--ok)}
.nv .abtn.ok:hover{background:#0c6540}
.nv .abtn.go{background:var(--vio);border-color:var(--vio);color:#fff}
.nv .abtn.go:hover{background:#4f2cab}
.nv .then{font-size:12px;color:var(--tx3);margin-top:9px}
/* ---------- Zeitleiste ---------- */
.tl{margin-top:13px}
.tlbar{display:flex;height:26px;border-radius:6px;overflow:hidden;background:var(--panel3)}
.tlseg{position:relative;border-right:1px solid var(--panel)}
.tlseg:last-child{border-right:none}
.tlseg.talk{background:#cdd7e4}
.tlseg.block{background:#adc0d8}
.tlvid{position:absolute;top:0;bottom:0;background:var(--vio);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#fff}
.tlax{display:flex;justify-content:space-between;font-size:10px;color:var(--tx3);margin-top:3px;font-variant-numeric:tabular-nums}
/* ---------- Medien ---------- */
.med{display:flex;align-items:center;gap:8px;padding:7px 9px;border:1px solid var(--line);border-radius:8px;margin-bottom:6px;background:var(--panel)}
.med .k{font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--tx3);width:86px;flex:none;margin-left:4px}
.med .id{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px;font-weight:650}
.med .ln{font-size:12px;color:var(--tx2)}
.med .at{font-size:12px;color:var(--vio);font-weight:700;font-variant-numeric:tabular-nums}
.med.played{opacity:.55;background:var(--panel2)}
.med.prep{border-color:#a5d5bf;background:var(--okbg)}
.med.sync{border-color:#cbb8f5;background:var(--viobg)}
.mck{display:flex;align-items:center;gap:5px;flex:none;font-size:10.5px;font-weight:700;letter-spacing:.04em;
color:var(--tx3);border:1px solid var(--line);border-radius:6px;padding:4px 7px;background:var(--panel)}
.mck .bx{width:12px;height:12px;border-radius:3px;border:2px solid var(--tx3);flex:none;position:relative}
.mck:hover{border-color:var(--ok);color:var(--ok)}
.mck.on{color:var(--ok);border-color:var(--ok);background:var(--okbg)}
.mck.on .bx{background:var(--ok);border-color:var(--ok)}
.mck.on .bx:after{content:"";position:absolute;left:2.5px;top:-.5px;width:3px;height:7px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.mck.sy.on{color:var(--vio);border-color:var(--vio);background:var(--viobg)}
.mck.sy.on .bx{background:var(--vio);border-color:var(--vio)}
.mck.sy:hover{border-color:var(--vio);color:var(--vio)}
.mck.done.on{color:var(--acc);border-color:var(--acc);background:var(--accbg)}
.mck.done.on .bx{background:var(--acc);border-color:var(--acc)}
.mck.done:hover{border-color:var(--acc);color:var(--acc)}
.med.played .id{text-decoration:line-through}
/* ---------- Status ---------- */
.stat{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px}
.tg{display:flex;align-items:center;gap:7px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);font-size:13px;color:var(--tx2);font-weight:550}
.tg .bx{width:15px;height:15px;border-radius:4px;border:2px solid var(--tx3);flex:none;position:relative}
.tg.on{border-color:var(--ok);color:var(--ok);background:var(--okbg)}
.tg.on .bx{background:var(--ok);border-color:var(--ok)}
.tg.on .bx:after{content:"";position:absolute;left:3.5px;top:.5px;width:4px;height:8px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
/* ---------- Anweisungen ---------- */
.anw{border-left:4px solid var(--tx3);background:var(--panel2);border-radius:0 8px 8px 0;padding:9px 12px;margin-bottom:6px;display:flex;gap:9px;align-items:flex-start}
.anw .txt{flex:1;font-size:16px;line-height:1.35;white-space:pre-wrap;word-break:break-word}
.anw .txt.ed{cursor:text;border-radius:5px;margin:-2px -4px;padding:2px 4px}
.anw .txt.ed:hover{background:rgba(11,100,208,.09);box-shadow:0 0 0 1px rgba(11,100,208,.25)}
.anw.k-gelb{border-left-color:var(--warn);background:var(--warnbg)}
.anw.k-gelb .txt{color:#6d4100;font-weight:650}
.anw.k-gelb .pb{color:var(--warn);border-color:#eccb95}
.anw.k-rot{border-left-color:var(--crit);background:var(--critbg)}
.anw.k-rot .txt{color:#8e1810;font-weight:750}
.anw.k-rot .pb{color:var(--crit);border-color:#f0b4ae}
.anw.k-grau{border-left-color:#7d8b9c;background:var(--panel2)}
.anw.k-grau .txt{color:var(--tx2);font-weight:400;font-size:15px}
.anw.k-grau .pb{color:var(--tx3)}
.anw.k-blau{border-left-color:var(--acc);background:var(--accbg)}
.anw.k-blau .txt{color:#0a4a99;font-weight:600}
.anw.k-blau .pb{color:var(--acc);border-color:#b9d3f4}
.anw.k-vio{border-left-color:var(--vio);background:var(--viobg)}
.anw.k-vio .txt{color:#3f2494;font-weight:750;letter-spacing:.01em}
.anw.k-vio .pb{color:var(--vio);border-color:#cbb8f5}
.anw.k-vio .txt:before{content:"„";opacity:.5}
.anw.k-vio .txt:after{content:"“";opacity:.5}
.anw.k-gruen{border-left-color:var(--ok);background:var(--okbg)}
.anw.k-gruen .txt{color:#0b5c3b;font-weight:600}
.anw.k-gruen .pb{color:var(--ok);border-color:#a5d5bf}
.anw .chk{width:22px;height:22px;flex:none;border:1px solid var(--line);border-radius:6px;background:var(--panel);
display:flex;align-items:center;justify-content:center;margin-top:1px}
.anw .chk .bx{width:11px;height:11px;border-radius:3px;border:2px solid var(--tx3);position:relative}
.anw .chk:hover{border-color:var(--ok)}
.anw .chk.on{border-color:var(--ok);background:var(--okbg)}
.anw .chk.on .bx{background:var(--ok);border-color:var(--ok)}
.anw .chk.on .bx:after{content:"";position:absolute;left:2px;top:-1px;width:3px;height:7px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.anw .ord{display:flex;flex-direction:column;gap:2px;flex:none;margin-top:1px}
.anw .ord button{font-size:9px;line-height:1;padding:3px 5px;border:1px solid var(--line);border-radius:4px;color:var(--tx3);background:var(--panel)}
.anw .ord button:hover:not(:disabled){border-color:var(--acc);color:var(--acc)}
.anw .ord button:disabled{opacity:.3;cursor:default}
.anw .x{color:var(--tx3);font-size:17px;line-height:1;padding:0 2px;flex:none}
.anw .x:hover{color:var(--crit)}
.row{display:flex;gap:7px;margin-top:8px}
textarea.inp{flex:1;resize:vertical;min-height:44px;font-size:15px}
.add{padding:0 15px;border-radius:8px;background:var(--acc);color:#fff;font-weight:700;font-size:14px}
.chips{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px;align-items:center}
.chip{font-size:12px;padding:4px 9px;border-radius:14px;border:1px solid var(--line);background:var(--panel);color:var(--tx2);font-weight:650}
.chip:hover{border-color:var(--acc);color:var(--acc)}
.chip.cut{border-color:#f0b4ae;color:var(--crit)}
.chip.auto{border-color:#b9d3f4;color:var(--acc)}
.chip.lbl{border:none;background:none;color:var(--tx3);font-size:10px;letter-spacing:.11em;padding-left:0;font-weight:700}
.prio{display:flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;flex:none;background:var(--panel)}
.prio button{padding:0 11px;font-size:12px;color:var(--tx3);font-weight:650}
.prio button.on{background:var(--tx3);color:#fff}
.prio button[data-p="1"].on{background:var(--warn)}
.prio button[data-p="2"].on{background:var(--crit)}
.prio button[data-p="3"].on{background:#7d8b9c}
.prio button[data-p="4"].on{background:var(--vio)}
.prio button[data-p="5"].on{background:var(--ok)}
/* ---------- Disposition / Ablaufplan ---------- */
.disp{font-size:14.5px;line-height:1.5}
.disp .h{font-weight:750;font-size:13.5px;color:var(--acc);margin:16px 0 6px}
.disp .h:first-child{margin-top:0}
.disp .p{color:var(--tx2);margin:2px 0}
.disp .p.l0{color:var(--tx)}
.disp .l1{padding-left:16px;font-size:14px}
.disp .l2{padding-left:32px;font-size:13.5px;color:var(--tx3)}
.disp .l3,.disp .l4{padding-left:46px;font-size:13px;color:var(--tx3)}
.disp .cue{margin:10px 0;padding:9px 12px;border-radius:8px;background:var(--viobg);border:1px solid #cbb8f5;color:var(--vio);font-weight:700;font-size:14.5px}
.disp .lies{margin:4px 0;color:var(--warn);font-size:13.5px;font-weight:650}
.hint{font-size:12.5px;color:#6d4100;background:var(--warnbg);border:1px solid #eccb95;border-radius:8px;padding:8px 11px;margin-bottom:12px}
.sw{display:flex;gap:4px;background:var(--panel2);padding:3px;border-radius:8px}
.sw button{padding:5px 11px;border-radius:6px;font-size:12.5px;color:var(--tx2);font-weight:700}
.sw button.on{background:var(--acc);color:#fff}
/* ---------- Editor ---------- */
.ed h4{margin:0 0 9px;font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:9px}
.fl{display:flex;flex-direction:column;gap:4px}
.fl label{font-size:11px;color:var(--tx3);font-weight:700;letter-spacing:.05em}
.fl input,.fl select{width:100%}
.ta{width:100%;resize:vertical;font-size:14px;line-height:1.45}
.mono{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px}
table.mt{width:100%;border-collapse:collapse}
table.mt th{font-size:10px;letter-spacing:.08em;color:var(--tx3);text-align:left;font-weight:700;padding:0 4px 5px}
table.mt td{padding:2px 4px}
table.mt input,table.mt select{width:100%;padding:5px 7px}
.bt{padding:7px 12px;border-radius:8px;border:1px solid var(--line);background:var(--panel);font-size:13px;font-weight:650;color:var(--tx2)}
.bt:hover{border-color:var(--acc);color:var(--acc)}
.bt.pri{background:var(--acc);color:#fff;border-color:transparent}
.bt.dan{color:var(--crit);border-color:#f0b4ae}
.bt.dan:hover{background:var(--critbg)}
.syn{font-size:12px;color:var(--tx2);background:var(--panel2);border-radius:8px;padding:9px 11px;line-height:1.6;margin-top:8px}
.mi{width:100%;text-align:left;padding:8px 10px;border-radius:7px;font-size:13.5px;color:var(--tx2);cursor:pointer}
.mi:hover{background:var(--panel2);color:var(--tx)}
.syn code{font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:0 4px}
/* ---------- Sonstiges ---------- */
.nxt{display:flex;gap:11px;align-items:baseline;padding:10px 13px;border:1px dashed var(--line);border-radius:var(--r);color:var(--tx2);font-size:13.5px;margin-bottom:12px;background:var(--panel)}
.nxt b{color:var(--tx);font-weight:650}
.crew{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:7px}
.cw{display:flex;flex-direction:column;gap:1px;padding:7px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}
.cw .ro{font-size:10px;letter-spacing:.09em;color:var(--tx3);font-weight:700}
.cw .na{font-size:15px;font-weight:650}
.chip.tm{border-color:#b9d3f4;color:var(--acc);background:var(--accbg)}
.chip.tm:hover{background:var(--acc);color:#fff;border-color:var(--acc)}
.reg{font-size:13px;color:var(--tx2);line-height:1.55}
.reg table{border-collapse:collapse;width:100%;margin-bottom:9px}
.reg td{padding:3px 8px 3px 0;border-bottom:1px solid var(--line);vertical-align:top}
.reg td:first-child{color:var(--tx3);width:52%}
.reg b{color:var(--tx)}
details>summary{cursor:pointer;list-style:none;padding:9px 13px;font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700}
details>summary::-webkit-details-marker{display:none}
details>summary:before{content:"▸ "}
details[open]>summary:before{content:"▾ "}
.raw{font-size:13.5px;color:var(--tx2);line-height:1.5}
.raw p{margin:0 0 8px}
.empty{color:var(--tx3);font-size:13px;font-style:italic}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:#c8d0da;border-radius:5px}
@media(max-width:1200px){#right{width:40%;min-width:300px}}
@media(max-width:980px){#side,#right{display:none}header{flex-wrap:wrap}}
</style>
</head>
<body>
<header>
<div class="clock" id="clock">--:--:--<small id="today">TD-PULT</small></div>
<div class="ttl" id="ttl"></div>
<div class="tabs" id="tabs"></div>
<button class="hbtn" id="prevBtn">◀</button>
<button class="hbtn" id="nextBtn">▶</button>
<button class="hbtn" id="nowBtn">Jetzt</button>
<button class="hbtn edit" id="modeBtn">Editieren</button>
<button class="hbtn" id="bauBtn">Programm bearbeiten</button>
<div class="spacer"></div>
<div class="motto" id="motto"></div>
<div style="position:relative">
<button class="hbtn" id="menuBtn">Dateien ▾</button>
<div id="menu" style="display:none;position:absolute;right:0;top:38px;z-index:9;background:var(--panel);
border:1px solid var(--line);border-radius:10px;box-shadow:0 8px 26px rgba(20,30,45,.14);padding:6px;min-width:250px">
<button class="mi" data-fx="ne">Anweisungen sichern</button>
<label class="mi" style="display:block">Anweisungen laden<input type="file" data-fx="ni" accept=".json" hidden></label>
<div style="height:1px;background:var(--line);margin:5px 6px"></div>
<button class="mi" data-fx="pe">Programm sichern</button>
<label class="mi" style="display:block">Programm laden<input type="file" data-fx="pi" accept=".json" hidden></label>
</div>
</div>
</header>
<div id="app">
<div id="side"></div>
<div id="main"></div>
<div id="right">
<div id="rhead">
<span class="sw" id="rtabs">
<button data-r="dispo" class="on">Disposition</button>
<button data-r="co13">Ablaufplan</button>
<button data-r="allg">Allgemein</button>
</span>
<span style="flex:1"></span>
<span class="sw" id="dsw">
<button data-d="voll" class="on">voll</button>
<button data-d="kompakt">kompakt</button>
</span>
</div>
<div id="rbody"></div>
</div>
</div>
<script>
const SEED = {"version":1,"cfg":{"titel":"TD-Pult","veranstaltung":"","lokal":{"name":"","stadt":"","sprache":""},"lang2":{"aktiv":false,"name":"","kurz":""},"stream":{"aktiv":false,"quelle":""},"status":{"prep":"Medien & Cams vorbereitet","done":"Punkt erledigt"},"schritte":{"prep":"vorbereitet","sync":"sync","done":"abgespielt"},"kategorien":[{"name":"Normal","farbe":"blau","rolle":""},{"name":"Wichtig","farbe":"gelb","rolle":""},{"name":"Kritisch","farbe":"rot","rolle":""},{"name":"Notiz","farbe":"grau","rolle":""},{"name":"Keyword","farbe":"violett","rolle":""},{"name":"Prep","farbe":"gruen","rolle":"prep"}],"bausteine":[{"gruppe":"Quellen","werte":["1","2","3","Media","Media Backup","Stream","Stream Backup","Motto-Bild"]},{"gruppe":"Schnitt","werte":["CUT","AUTO"]},{"gruppe":"Standard","werte":["ready","Bühnentotale","Schwarzbild","auf Motto-Bild halten","Ton prüfen","Video vorbereiten","Redner steht"]}],"arten":["Vortrag","Interview","Musik","Video","Ankündigungen","Pause"],"medienarten":["Video","Musik","Bild","Stream","Sonstiges"],"prepExtra":["Kameras prüfen","Ton prüfen"],"regeln":[],"bezeichnungen":{"ablauf":"Ablaufplan","vorsitz":"Vorsitz","motto":"Tagesmotto","redner":"Redner"}},"general":{"body":[{"k":"h","lvl":0,"text":"SO IST DIESER BEREICH GEDACHT"},{"k":"p","lvl":0,"text":"Hier stehen die Absprachen, die für die ganze Schicht gelten und die man im Zweifel nachschlagen will."},{"k":"p","lvl":0,"text":"Zum Beispiel: Ablauf beim Umschalten auf einen angeschlossenen Sprachsektor, Auf- und Rückschalten eines Streams, Schnittregeln, Bezeichnungen der Quellen und Kameras."},{"k":"p","lvl":0,"text":"Bearbeiten über „Programm bearbeiten“ und dann rechts im Reiter „Allgemein bearbeiten“."},{"k":"h","lvl":0,"text":"SCHREIBWEISE"},{"k":"p","lvl":0,"text":"Eine Zeile, die mit # beginnt, wird eine Überschrift."},{"k":"p","lvl":1,"text":"Zwei Leerzeichen am Zeilenanfang rücken eine Zeile ein."}]},"shifts":[{"id":"schicht-1","label":"Schicht 1","motto":"","vorsitz":"","crew":[{"rolle":"Kamera 1","name":""},{"rolle":"Kamera 2","name":""},{"rolle":"Kamera 3","name":""},{"rolle":"Media","name":""},{"rolle":"Media Backup","name":""},{"rolle":"Stream / Fremdsprache","name":""},{"rolle":"Ton","name":""},{"rolle":"Bühne","name":""}],"items":[{"id":"beispiel-1","time":"09:00","nr":null,"art":"","titel":"Beispielpunkt – bearbeiten oder löschen","redner":"","dauer":null,"hinweis":"","kurz":"So sieht ein Programmpunkt aus","sync":false,"stream":false,"volltext":["Hier steht der offizielle Anweisungstext zu diesem Punkt. Bearbeitet wird er rechts unter „Ablaufplan bearbeiten“."],"mediaList":[],"segs":[],"gesamt":null,"dispo":null}]}]};
/* ================= Speicher ================= */
const KP = "td-programm-v1"; // Programmdaten (bearbeitbar)
const KN = "td-notizen-v1"; // Anweisungen + Status
const saveProg = () => localStorage.setItem(KP, JSON.stringify(P));
const saveNotes = () => localStorage.setItem(KN, JSON.stringify(S));
let P = loadProg();
let S = loadNotes();
if(!localStorage.getItem(KN)) saveNotes();
function clone(o){ return JSON.parse(JSON.stringify(o)); }
function loadProg(){
try{
const r = JSON.parse(localStorage.getItem(KP));
if(r && r.shifts){
// Abschnitte, die es beim Speichern noch nicht gab, aus der Auslieferung ergänzen
let add = false;
Object.keys(SEED).forEach(k => { if(k !== "shifts" && r[k] === undefined){ r[k] = clone(SEED[k]); add = true; } });
// fehlende Konfigurationswerte ergänzen, vorhandene nie überschreiben
const mergeIn = (t, d) => {
Object.keys(d || {}).forEach(k => {
if(t[k] === undefined){ t[k] = clone(d[k]); add = true; }
else if(d[k] && typeof d[k] === "object" && !Array.isArray(d[k])) mergeIn(t[k], d[k]);
});
};
r.cfg = r.cfg || {}; mergeIn(r.cfg, SEED.cfg || {});
if(r.general && !(r.general.body||[]).length && SEED.general){ r.general = clone(SEED.general); add = true; }
(r.shifts||[]).forEach((sh,i) => { if(!sh.crew){ sh.crew = clone((SEED.shifts[i]||SEED.shifts[0]).crew || []); add = true; } });
if(add) localStorage.setItem(KP, JSON.stringify(r));
return r;
}
}catch(e){}
return clone(SEED);
}
function loadNotes(){
try{ const r = JSON.parse(localStorage.getItem(KN)); if(r && r.notes) return r; }catch(e){}
return {notes:{}, status:{}, global:[]};
}
/* ================= Zustand ================= */
let shiftIdx = 0, curIdx = 0, prio = 0;
let dispMode = "voll", rTab = "dispo", editMode = true, bau = false, openLi = -1, editing = null, bauTab = "punkt";
/* Alles Folgende kommt aus der Konfiguration und ist im Bau-Modus änderbar. */
const C = () => P.cfg || (P.cfg = {});
const KAT = () => (C().kategorien && C().kategorien.length ? C().kategorien : [{name:"Normal",farbe:"grau"}]);
const CHIPS = () => (C().bausteine || []).map(g => [g.gruppe, g.werte || []]);
const ARTEN = () => ["", ...(C().arten || [])];
const MKIND = () => (C().medienarten && C().medienarten.length ? C().medienarten : ["Video"]);
const L2 = () => C().lang2 || {};
const useSync= () => !!L2().aktiv;
const ST2 = () => C().stream || {};
const useStream = () => ST2().aktiv !== false && (ST2().aktiv || !!ST2().quelle);
const STEPS = () => Object.assign({prep:"vorbereitet", sync:"sync", done:"abgespielt"}, C().schritte || {});
const LBL = k => (C().bezeichnungen || {})[k] || {ablauf:"Ablaufplan", vorsitz:"Vorsitz", motto:"Tagesmotto", redner:"Redner"}[k];
const FARBEN = {grau:"k-grau", gelb:"k-gelb", rot:"k-rot", blau:"k-blau", violett:"k-vio", gruen:"k-gruen"};
const katCls = i => FARBEN[(KAT()[i]||{}).farbe] || "k-grau";
const katNam = i => ((KAT()[i]||{}).name || "?");
/* ================= Hilfen ================= */
const $ = s => document.querySelector(s);
const esc = s => (s==null?"":String(s)).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
const mins = t => { const m = /^(\d{1,2}):(\d{2})/.exec(t||""); return m ? (+m[1])*60 + (+m[2]) : 0; };
const hhmm = m => String(Math.floor((((m%1440)+1440)%1440)/60)).padStart(2,"0") + ":" + String(Math.round(m)%60).padStart(2,"0");
const shift = () => P.shifts[shiftIdx];
const items = () => shift().items;
const item = () => items()[curIdx] || items()[0];
const st = id => { const o = S.status[id] || (S.status[id] = {prep:false, sync:false, played:false, vids:{}});
if(!o.vids) o.vids = {}; if(!o.vprep) o.vprep = {}; if(!o.vsync) o.vsync = {}; return o; };
const mPrep = (id,j) => { const s = S.status[id]; return !!(s && s.vprep && s.vprep["m"+j]); };
const mSync = (id,j) => { const s = S.status[id]; return !!(s && s.vsync && s.vsync["m"+j]); };
const mDone = (id,j) => { const s = S.status[id]; return !!(s && s.vids && s.vids["m"+j]); };
const nt = id => S.notes[id] || (S.notes[id] = []);
function nowMin(){ const d = new Date(); return d.getHours()*60 + d.getMinutes() + d.getSeconds()/60; }
function nearestIdx(){
const m = nowMin(), it = items(); let idx = 0;
for(let i=0;i<it.length;i++) if(mins(it[i].time) <= m) idx = i;
return idx;
}
function uid(){ return "x" + Math.random().toString(36).slice(2,9); }
/* ================= Medien-Fahrplan ================= */
function cueList(){
const out = [];
items().forEach((x,i)=>{
const base = mins(x.time), pos = {};
(x.segs||[]).forEach(sg=>{ let off=0; (sg.videos||[]).forEach(v=>{ pos[v.n]=sg.start+off; off+=v.min; }); });
(x.mediaList||[]).forEach((m,j)=>{
out.push({i, j, m, item:x, id:x.id, est: base + (m.n!=null && pos[m.n]!=null ? pos[m.n] : 0)});
});
});
return out.sort((a,b)=>a.est-b.est);
}
const isDone = c => mDone(c.id, c.j);
function nextCue(list){
const l = list || cueList();
for(let i=0;i<l.length;i++) if(l[i].i >= curIdx && !isDone(l[i])) return {c:l[i], i};
for(let i=0;i<l.length;i++) if(!isDone(l[i])) return {c:l[i], i};
return null;
}
/* ================= Kopf ================= */
function drawHead(){
const d = new Date();
$("#clock").firstChild.nodeValue = [d.getHours(),d.getMinutes(),d.getSeconds()].map(v=>String(v).padStart(2,"0")).join(":");
$("#today").textContent = (shift().label||"").toUpperCase();
const cf = C(), lok = [(cf.lokal||{}).name, (cf.lokal||{}).stadt].filter(Boolean).join(" · ");
$("#ttl").innerHTML = `<b>${esc(cf.veranstaltung||"")}</b>${lok?`<span>${esc(lok)}</span>`:""}` +
(useSync()?`<span>+ ${esc(L2().name||"Fremdsprache")}</span>`:"");
document.title = [cf.titel||"TD-Pult", cf.veranstaltung, lok].filter(Boolean).join(" · ");
$("#motto").innerHTML = (shift().motto?esc(LBL("motto"))+": "+esc(shift().motto):"") +
(shift().vorsitz?"<br>"+esc(LBL("vorsitz"))+": <b style='color:var(--tx2)'>"+esc(shift().vorsitz)+"</b>":"");
}
function drawTabs(){
$("#tabs").innerHTML = P.shifts.map((s,i)=>`<button data-i="${i}" class="${i===shiftIdx?"on":""}">${esc(s.label)}</button>`).join("")
+ (bau?`<button data-new="1" title="Neue Schicht">+</button>`:"");
$("#tabs").querySelectorAll("[data-i]").forEach(b => b.onclick = () => {
shiftIdx = +b.dataset.i; curIdx = 0; openLi = -1; drawAll();
});
const nb = $("#tabs").querySelector("[data-new]");
if(nb) nb.onclick = () => {
P.shifts.push({id:uid(), label:"Neue Schicht", motto:"", vorsitz:"", items:[]});
shiftIdx = P.shifts.length-1; curIdx = 0; saveProg(); drawAll();
};
}
function syncBtns(){
$("#modeBtn").textContent = editMode ? "Editieren" : "Ansicht";
$("#modeBtn").classList.toggle("edit", editMode);
$("#modeBtn").classList.toggle("on", !editMode);
$("#bauBtn").classList.toggle("bau", bau);
$("#bauBtn").textContent = bau ? "Programm: Bearbeiten" : "Programm bearbeiten";
$("#modeBtn").style.display = bau ? "none" : "";
}
/* ================= Ablaufliste ================= */
function drawSide(){
let h = `<div class="sh">ABLAUF · ${esc((shift().label||"").toUpperCase())}</div>`;
items().forEach((x,i)=>{
const s = S.status[x.id]||{}, n = (S.notes[x.id]||[]).length;
const nl = S.notes[x.id]||[];
const rank = {rot:4, gelb:3, violett:2, gruen:1};
const cmap = {rot:"var(--crit)", gelb:"var(--warn)", violett:"var(--vio)", gruen:"var(--ok)", blau:"var(--acc)", grau:"var(--acc)"};
let best = "", bv = 0;
nl.forEach(a=>{ const f = (KAT()[a.p||0]||{}).farbe; const v = rank[f]||0; if(v > bv){ bv = v; best = f; } });
const col = best ? cmap[best] : n ? "var(--acc)" : "transparent";
const facts = x.titel ? [x.redner, x.dauer?x.dauer+" Min.":"", x.kurz].filter(Boolean).join(" · ") : "";
const open = openLi === i;
h += `<div class="li ${i===curIdx?"on":""} ${s.played?"done":""}">
<div class="t" data-go="${i}">${esc(x.time)}</div>
<div style="flex:1;min-width:0" data-go="${i}">
<div class="n">${esc(x.titel || x.kurz || "Programmpunkt")}</div>
${facts?`<div class="f">${esc(facts)}</div>`:""}
<div class="m">${x.nr?`<span>NR.&nbsp;${x.nr}</span>`:""}${x.sync?"<span>SYNC</span>":""}${x.stream?"<span>STREAM</span>":""}${n?`<span>${n}&nbsp;ANW.</span>`:""}</div>
${open?`<div class="full">${(x.volltext||[]).map(t=>`<p>${esc(t)}</p>`).join("") || "<p>Kein weiterer Anweisungstext.</p>"}</div>`:""}
${bau?`<div class="tools">
<button data-up="${i}" ${i===0?"disabled":""}>▲</button>
<button data-dn="${i}" ${i===items().length-1?"disabled":""}>▼</button>
<button data-dup="${i}">Kopie</button>
<button data-del="${i}">Löschen</button></div>`:""}
</div>
<div style="display:flex;flex-direction:column;align-items:center;gap:4px">
<span class="dot" style="background:${col}"></span>
<button class="exp" data-exp="${i}">${open?"▾":"▸"}</button>
</div></div>`;
});
if(bau) h += `<div style="padding:12px"><button class="bt pri" id="addItem" style="width:100%">+ Neuer Programmpunkt</button></div>`;
h += `<div class="sh" style="padding-top:14px;line-height:1.7;letter-spacing:0;font-weight:400">
← → &nbsp;Punkt wechseln · E &nbsp;Modus<br>
Enter &nbsp;Anweisung speichern<br>
Alles wird lokal im Browser gesichert.</div>`;
$("#side").innerHTML = h;
$("#side").querySelectorAll("[data-go]").forEach(e => e.onclick = () => { curIdx = +e.dataset.go; drawAll(); });
$("#side").querySelectorAll("[data-exp]").forEach(e => e.onclick = ev => {
ev.stopPropagation(); openLi = openLi === +e.dataset.exp ? -1 : +e.dataset.exp; drawSide();
});
const A = $("#addItem");
if(A) A.onclick = () => {
const it = items(), ref = it[curIdx];
const nu = {id:uid(), time: ref?ref.time:"12:00", nr:null, art:"", titel:"Neuer Punkt", redner:"",
dauer:null, hinweis:"", kurz:"", sync:false, stream:false,
volltext:[], mediaList:[], segs:[], gesamt:null, dispo:null};
it.splice(curIdx+1, 0, nu); curIdx = curIdx+1; saveProg(); drawAll();
};
$("#side").querySelectorAll("[data-up]").forEach(b => b.onclick = ev => { ev.stopPropagation(); mv(+b.dataset.up,-1); });
$("#side").querySelectorAll("[data-dn]").forEach(b => b.onclick = ev => { ev.stopPropagation(); mv(+b.dataset.dn, 1); });
$("#side").querySelectorAll("[data-dup]").forEach(b => b.onclick = ev => {
ev.stopPropagation(); const i = +b.dataset.dup;
const c = clone(items()[i]); c.id = uid(); c.titel = (c.titel||"") + " (Kopie)";
items().splice(i+1, 0, c); curIdx = i+1; saveProg(); drawAll();
});
$("#side").querySelectorAll("[data-del]").forEach(b => b.onclick = ev => {
ev.stopPropagation(); const i = +b.dataset.del;
if(!confirm("Programmpunkt „" + (items()[i].titel || items()[i].time) + "“ löschen?")) return;
items().splice(i,1); curIdx = Math.max(0, Math.min(curIdx, items().length-1)); saveProg(); drawAll();
});
}
function mv(i,d){
const it = items(), j = i+d;
if(j < 0 || j >= it.length) return;
[it[i], it[j]] = [it[j], it[i]];
if(curIdx === i) curIdx = j; else if(curIdx === j) curIdx = i;
saveProg(); drawAll();
}
function shorten(s){ s = s||""; return s.length > 74 ? s.slice(0,74).replace(/\s\S*$/,"")+" …" : s; }
/* ================= Mitte ================= */
function drawMain(){ bau ? drawEditor() : drawLive(); }
function drawLive(){
const x = item(), s = st(x.id);
let h = `<div class="now">
<div class="kick">
<span class="time">${esc(x.time)}</span>
${x.art?`<span class="badge art">${esc(x.art).toUpperCase()}${x.nr?" · NR. "+x.nr:""}</span>`:""}
${x.sync&&useSync()?`<span class="badge sync">SYNC ${esc((L2().kurz||L2().name||"").toUpperCase())}</span>`:""}
${x.stream&&useStream()?`<span class="badge stream">STREAM ${esc((ST2().quelle||"").toUpperCase())}</span>`:""}
${x.dauer?`<span class="badge">${x.dauer} MIN</span>`:""}
</div>
<h1>${esc(x.titel || x.kurz || "Programmpunkt")}</h1>
${x.redner||x.hinweis?`<div class="sub">${x.redner?`${esc(LBL("redner"))}: <b style="color:var(--tx)">${esc(x.redner)}</b>`:""}${x.hinweis?` · ${esc(x.hinweis)}`:""}</div>`:""}
${x.titel && x.kurz?`<div class="facts">${esc(x.kurz)}</div>`:""}
${regelHinweise(x)}
<div class="stat">
<button class="tg ${s.prep?"on":""}" data-t="prep"><span class="bx"></span>${esc((C().status||{}).prep||"vorbereitet")}</button>
<button class="tg ${s.played?"on":""}" data-t="played"><span class="bx"></span>${esc((C().status||{}).done||"erledigt")}</button>
</div>
</div>`;
h += nvHTML();
if(editMode || S.global.length)
h += `<div class="card"><div class="hd"><h3>GLOBALE ANWEISUNGEN · GANZE SCHICHT</h3></div><div class="bd">
${listHTML(S.global, "g")}${editMode?inputHTML("g"):""}</div></div>`;
const na = nt(x.id), nd = na.filter(a=>a.d).length;
h += `<div class="card"><div class="hd"><h3>MEINE ANWEISUNGEN · ${esc(x.time)}</h3>
${na.length?`<span style="flex:1"></span><span class="badge" style="${nd===na.length?"background:var(--okbg);color:var(--ok);border-color:#a5d5bf":""}">${nd}/${na.length} ERLEDIGT</span>`:""}
</div><div class="bd">
${listHTML(nt(x.id), "i")}${editMode?inputHTML("i"):""}</div></div>`;
const nx = items()[curIdx+1];
if(nx) h += `<div class="nxt"><b>${esc(nx.time)}</b><span>Als Nächstes: ${esc(nx.titel || nx.kurz || "Programmpunkt")}</span>
${nx.sync?'<span class="badge sync">SYNC</span>':""}${nx.stream?'<span class="badge stream">STREAM</span>':""}</div>`;
h += crewHTML();
$("#main").innerHTML = h;
bindLive();
}
function regelHinweise(x){
const hay = ((x.titel||"") + " " + (x.kurz||"") + " " + (x.art||"")).toLowerCase();
return (C().regeln || []).filter(r => r.suche && hay.indexOf(String(r.suche).toLowerCase()) >= 0)
.map(r => `<div class="hint" style="margin:12px 0 0">${esc(r.text)}</div>`).join("");
}
function crewHTML(){
const c = (shift().crew||[]).filter(p=>p.name && p.name.trim());
if(!c.length) return "";
return `<div class="card"><div class="hd"><h3>BESETZUNG · ${esc((shift().label||"").toUpperCase())}</h3></div>
<div class="bd"><div class="crew">${c.map(p=>
`<div class="cw"><span class="ro">${esc(p.rolle)}</span><span class="na">${esc(p.name)}</span></div>`).join("")}
</div></div></div>`;
}
function nvHTML(){
const l = cueList(), r = nextCue(l);
if(!r) return `<div class="nv"><div class="lbl">NÄCHSTES VIDEO</div>
<div class="meta">Keine offenen Medien in dieser Schicht.</div></div>`;
const c = r.c;
let then = null;
for(let i=r.i+1;i<l.length;i++) if(!isDone(l[i])){ then = l[i]; break; }
const diff = c.est - nowMin();
const inTxt = diff > 0.5 ? "in " + Math.round(diff) + " Min." : diff > -1 ? "jetzt" : "überfällig";
return `<div class="nv">
<div class="lbl">NÄCHSTES VIDEO · ANKÜNDIGEN &amp; VORBEREITEN</div>
<div class="line">
<span class="id">${esc(c.m.id)}</span>
<span class="eta">ca. ${hhmm(c.est)}</span>
<span class="badge">${esc(inTxt)}</span>
${c.m.sync?'<span class="badge sync">SYNC-DATEI</span>':""}${c.m.stream?'<span class="badge stream">STREAM</span>':""}
</div>
<div class="meta">${c.m.n?("Video "+c.m.n):esc(c.m.kind)}${c.m.len?" · "+esc(c.m.len):""} — ${esc(c.item.titel || c.item.kurz || "")} (${esc(c.item.time)})</div>
<div class="steps">
<button class="stp ${mPrep(c.id,c.j)?"on":""}" data-nv="prep"><b>1</b>${mPrep(c.id,c.j)?esc(STEPS().prep)+" ✓":esc(STEPS().prep)}</button>
${useSync()?`<span class="sep"></span>
<button class="stp ${mSync(c.id,c.j)?"on":""}" data-nv="sync"><b>2</b>${mSync(c.id,c.j)?esc(STEPS().sync)+" bestätigt ✓":esc(STEPS().sync)+" bestätigen"}</button>`:""}
<span class="sep"></span>
<button class="stp go" data-nv="done"><b>${useSync()?3:2}</b>${esc(STEPS().done)} → weiter</button>
</div>
<div class="acts">
<span style="flex:1"></span>
<button class="abtn" data-nv="go">Zum Punkt springen</button>
${editMode?`<button class="abtn" data-nv="ansage">Ansage übernehmen</button>`:""}
</div>
${then?`<div class="then">Danach: <b style="color:var(--tx2)">${esc(then.m.id)}</b> ca. ${hhmm(then.est)}${then.m.len?" · "+esc(then.m.len):""}</div>`:""}
</div>`;
}
function tlHTML(x){
const tot = x.gesamt || (x.segs||[]).reduce((a,s)=>a+s.min,0);
if(!x.segs || !x.segs.length || !tot) return "";
let bar = "";
x.segs.forEach(sg=>{
let inner = "", off = 0;
(sg.videos||[]).forEach(v=>{
inner += `<div class="tlvid" style="left:${(off/sg.min)*100}%;width:${(v.min/sg.min)*100}%">${v.n}</div>`;
off += v.min;
});
bar += `<div class="tlseg ${sg.kind}" style="width:${(sg.min/tot)*100}%">${inner}</div>`;
});
return `<div class="tl"><div class="tlbar">${bar}</div>
<div class="tlax"><span>${esc(x.time)}</span><span>violett = Video</span><span>${hhmm(mins(x.time)+tot)}</span></div></div>`;
}
/* ================= Rechte Spalte ================= */
function drawRight(){
const x = item(), s = st(x.id);
$("#dsw").style.display = (!bau && rTab === "dispo" && x.dispo) ? "" : "none";
$("#rtabs").querySelectorAll("button")[0].textContent = bau ? "Disposition bearbeiten" : "Disposition";
$("#rtabs").querySelectorAll("button")[1].textContent = bau ? esc(LBL("ablauf"))+" bearbeiten" : esc(LBL("ablauf"));
$("#rtabs").querySelectorAll("button")[2].textContent = bau ? "Allgemein bearbeiten" : "Allgemein";
$("#rtabs").querySelectorAll("button").forEach(b => b.classList.toggle("on", b.dataset.r === rTab));
$("#dsw").querySelectorAll("button").forEach(b => b.classList.toggle("on", b.dataset.d === dispMode));
if(bau){ drawRightEditor(); return; }
let h = "";
if(rTab === "allg"){
h = `<div style="font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700;margin-bottom:10px">
ALLGEMEINE ANWEISUNGEN · SYNC, STREAM UND KONVENTION</div>
<div class="disp">${dispHTML(P.general || {body:[]})}</div>`;
$("#rbody").innerHTML = h; return;
}
if(rTab === "dispo"){
if(!x.dispo) h = `<div class="empty">Zu diesem Punkt gibt es keine Disposition. Über „Programm bearbeiten“ lässt sich eine anlegen.</div>`;
else h = `<div style="font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700;margin-bottom:10px">
DISPOSITION${x.nr?" NR. "+x.nr:""}${x.redner?" · "+esc(x.redner):""}</div><div class="disp">${dispHTML(x.dispo)}</div>`;
} else {
const base = mins(x.time), pos = {};
(x.segs||[]).forEach(sg=>{ let off=0; (sg.videos||[]).forEach(v=>{ pos[v.n]=sg.start+off; off+=v.min; }); });
h = `<div style="font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700;margin-bottom:10px">${esc(LBL("ablauf").toUpperCase())} · ${esc(x.time)}</div>`;
if((x.mediaList||[]).length){
h += `<div style="margin-bottom:12px">`;
x.mediaList.forEach((m,i)=>{
const est = base + (m.n!=null && pos[m.n]!=null ? pos[m.n] : 0);
const pv = mPrep(x.id,i), sv = mSync(x.id,i), dv = mDone(x.id,i);
h += `<div class="med ${dv?"played":""} ${!dv&&sv&&useSync()?"sync":!dv&&pv?"prep":""}">
<button class="mck ${pv?"on":""}" data-mp="${i}"><span class="bx"></span>${esc(STEPS().prep)}</button>
${useSync()?`<button class="mck sy ${sv?"on":""}" data-ms="${i}" title="${esc(L2().name||"")}"><span class="bx"></span>${esc(STEPS().sync)}</button>`:""}
<button class="mck done ${dv?"on":""}" data-md="${i}"><span class="bx"></span>${esc(STEPS().done)}</button>
<span class="k">${m.n?("VIDEO "+m.n):esc(m.kind).toUpperCase()}</span>
<span class="id">${esc(m.id)}</span>
<span class="ln">${esc(m.len)}</span>
<span style="flex:1"></span>
<span class="at">ca. ${hhmm(est)}</span>
${m.sync?'<span class="badge sync">SYNC</span>':""}${m.stream?'<span class="badge stream">STREAM</span>':""}
</div>`;
});
h += `</div>`;
}
const txt = (x.volltext||[]).filter(Boolean);
h += txt.length ? `<div class="raw">${txt.map(t=>`<p>${esc(t)}</p>`).join("")}</div>`
: `<div class="empty">Kein eigener Anweisungstext zu diesem Punkt.</div>`;
const pv = items()[curIdx-1];
if(pv && (pv.volltext||[]).length){
h += `<div style="margin-top:18px;font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700">
DAVOR · ${esc(pv.time)} · ${esc(pv.titel || pv.kurz || "")}</div>
<div class="raw" style="opacity:.75;margin-top:8px">${pv.volltext.map(t=>`<p>${esc(t)}</p>`).join("")}</div>`;
}
}
$("#rbody").innerHTML = h;
$("#rbody").querySelectorAll("[data-mp]").forEach(b => b.onclick = () => {
const k = "m"+b.dataset.mp; s.vprep[k] = !s.vprep[k]; saveNotes(); drawMain(); drawRight(); drawSide();
});
$("#rbody").querySelectorAll("[data-ms]").forEach(b => b.onclick = () => {
const k = "m"+b.dataset.ms; s.vsync[k] = !s.vsync[k];
if(s.vsync[k]) s.vprep[k] = true;
saveNotes(); drawMain(); drawRight(); drawSide();
});
$("#rbody").querySelectorAll("[data-md]").forEach(b => b.onclick = () => {
const k = "m"+b.dataset.md; s.vids[k] = !s.vids[k];
if(s.vids[k]){ s.vprep[k] = true; s.vsync[k] = true; }
saveNotes(); drawMain(); drawRight(); drawSide();
});
}
function dispHTML(t){
let h = "";
if(t.hinweis) h += `<div class="hint"><b>Hinweis (${esc(LBL("redner"))}):</b> ${esc(t.hinweis)}</div>`;
(t.body||[]).forEach(b=>{
if(b.k === "h") h += `<div class="h">${esc(b.text)}</div>`;
else if(b.k === "cue") h += `<div class="cue">▶ ${esc(b.text)}</div>`;
else if(b.k === "lies")h += `<div class="lies">${esc(b.text)}</div>`;
else if(dispMode === "voll" || b.lvl === 0)
h += `<div class="p l${Math.min(b.lvl||0,4)} ${(b.lvl||0)===0?"l0":""}">${esc(b.text)}</div>`;
});
return h;
}
/* ================= Anweisungen ================= */
function listHTML(arr, scope){
if(!arr.length) return `<div class="empty">Noch keine Anweisung eingetragen.</div>`;
return arr.map((a,i)=>{
const chk = `<button class="chk ${a.d?"on":""}" data-chk="${scope}:${i}" title="erledigt"><span class="bx"></span></button>`;
if(editMode && editing && editing.s === scope && editing.i === i)
return `<div class="anw ${katCls(a.p||0)}" style="align-items:stretch">${chk}
<div class="prio" data-eprio style="flex-direction:column;height:auto">
${KAT().map((k,q)=>`<button data-ep="${q}" class="${(a.p||0)===q?"on":""} ${katCls(q)}" style="padding:4px 8px">${esc(k.name)}</button>`).join("")}
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:7px">
<textarea class="inp" id="ed-note" style="min-height:62px">${esc(a.t)}</textarea>
<div style="display:flex;gap:7px">
<button class="bt pri" data-esave="1">Speichern</button>
<button class="bt" data-ecancel="1">Abbrechen</button>
<span style="flex:1"></span>
<button class="bt dan" data-del="${scope}:${i}">Löschen</button>
</div>
</div></div>`;
return `<div class="anw ${katCls(a.p||0)} ${a.d?"done":""}">${chk}
${editMode?`<div class="ord">
<button data-mvu="${scope}:${i}" ${i===0?"disabled":""} title="nach oben">▲</button>
<button data-mvd="${scope}:${i}" ${i===arr.length-1?"disabled":""} title="nach unten">▼</button>
</div>`:""}
${editMode?`<button class="pb" data-cyc="${scope}:${i}">${esc(katNam(a.p||0).toUpperCase())}</button>`
:`<span class="pb">${esc(katNam(a.p||0).toUpperCase())}</span>`}
<div class="txt${editMode?" ed":""}" ${editMode?`data-edit="${scope}:${i}" title="Zum Bearbeiten klicken"`:""}>${esc(a.t)}</div>
${editMode?`<button class="x" data-del="${scope}:${i}">×</button>`:""}</div>`;
}).join("");
}
function inputHTML(scope){
const crew = (shift().crew||[]).filter(p=>p.name && p.name.trim());
const team = crew.length ? `<div class="chips">
<span class="chip lbl">TEAM</span>
${crew.map(p=>`<button class="chip tm" data-chip="${scope}|${esc(p.name.trim())}" title="${esc(p.rolle)}">${esc(p.name.trim())}</button>`).join("")}
</div>` : "";
const chips = CHIPS().map(([lbl,cs]) =>
`<span class="chip lbl">${lbl.toUpperCase()}</span>` +
cs.map(c=>`<button class="chip" data-chip="${scope}|${esc(c)}">${esc(c)}</button>`).join("")
).join(`<span style="width:10px"></span>`);
return `<div class="row">
<textarea class="inp" id="in-${scope}" placeholder="Anweisung, Notiz oder Keyword …"></textarea>
<button class="add" data-add="${scope}">+</button>
</div>
<div class="chips" style="margin-top:8px">
<span class="chip lbl">KATEGORIE</span>
<div class="prio" data-scope="${scope}">
${KAT().map((k,p)=>`<button data-p="${p}" class="${prio===p?"on":""} ${katCls(p)}">${esc(k.name)}</button>`).join("")}
</div>
</div>
${team}
<div class="chips">${chips}</div>
${scope === "i" && prepIdx() >= 0 ? `<div class="chips"><button class="chip" data-prepgen="1" style="border-color:#a5d5bf;color:var(--ok)">+ ${esc(katNam(prepIdx()))}-Aufgaben aus den Medien anlegen</button></div>` : ""}`;
}
function bindLive(){
const x = item(), s = st(x.id), M = $("#main");
M.querySelectorAll(".tg").forEach(b => b.onclick = () => { s[b.dataset.t] = !s[b.dataset.t]; saveNotes(); drawAll(); });
M.querySelectorAll("[data-nv]").forEach(b => b.onclick = () => {
const r = nextCue(); if(!r) return;
const c = r.c;
if(b.dataset.nv === "done"){ const o = st(c.id); o.vids["m"+c.j] = true; o.vprep["m"+c.j] = true; o.vsync["m"+c.j] = true; saveNotes(); drawAll(); }
if(b.dataset.nv === "prep"){ const o = st(c.id); o.vprep["m"+c.j] = !o.vprep["m"+c.j]; saveNotes(); drawAll(); }
if(b.dataset.nv === "sync"){ const o = st(c.id); o.vsync["m"+c.j] = !o.vsync["m"+c.j];
if(o.vsync["m"+c.j]) o.vprep["m"+c.j] = true; saveNotes(); drawAll(); }
if(b.dataset.nv === "go"){ curIdx = c.i; drawAll(); }
if(b.dataset.nv === "ansage"){
nt(item().id).push({t:`Nächstes Video ${c.m.id}${c.m.len?" ("+c.m.len+")":""} vorbereiten — ca. ${hhmm(c.est)}`, p:1});
saveNotes(); drawAll();
}
});
M.querySelectorAll("[data-chip]").forEach(b => b.onclick = () => {
const [scope, txt] = b.dataset.chip.split("|");
const el = $("#in-"+scope);
const s0 = el.selectionStart ?? el.value.length, e0 = el.selectionEnd ?? el.value.length;
const before = el.value.slice(0,s0), after = el.value.slice(e0);
const ins = (before && !/[\s(]$/.test(before) ? " " : "") + txt + " ";
el.value = before + ins + after; el.focus();
try{ el.setSelectionRange(before.length+ins.length, before.length+ins.length); }catch(err){}
});
M.querySelectorAll(".prio button").forEach(b => b.onclick = () => {
prio = +b.dataset.p;
M.querySelectorAll(".prio button").forEach(o => o.classList.toggle("on", +o.dataset.p === prio));
});
M.querySelectorAll("[data-add]").forEach(b => b.onclick = () => addNote(b.dataset.add));
M.querySelectorAll("[data-prepgen]").forEach(b => b.onclick = () => {
const x = item(), arr = nt(x.id);
(x.mediaList||[]).forEach(m=>{
const t = `${m.id||m.kind} ${m.len?"("+m.len+") ":""}vorbereiten`;
if(!arr.some(a=>a.t === t)) arr.push({t, p:Math.max(0,prepIdx()), d:false});
});
(C().prepExtra || []).forEach(e=>{ if(e && !arr.some(a=>a.t === e)) arr.push({t:e, p:Math.max(0,prepIdx()), d:false}); });
saveNotes(); drawAll();
});
["g","i"].forEach(sc => {
const el = $("#in-"+sc);
if(el) el.onkeydown = e => { if(e.key === "Enter" && (e.metaKey||e.ctrlKey||!e.shiftKey)){ e.preventDefault(); addNote(sc); } };
});
M.querySelectorAll("[data-cyc]").forEach(b => b.onclick = () => {
const [sc,i] = b.dataset.cyc.split(":");
const arr = sc === "g" ? S.global : nt(item().id);
arr[+i].p = ((arr[+i].p||0)+1) % KAT().length; saveNotes(); drawAll();
});
M.querySelectorAll("[data-del]").forEach(b => b.onclick = () => {
const [sc,i] = b.dataset.del.split(":");
const arr = sc === "g" ? S.global : nt(item().id);
arr.splice(+i,1); saveNotes(); drawAll();
});
const arrOf = sc => sc === "g" ? S.global : nt(item().id);
M.querySelectorAll("[data-chk]").forEach(b => b.onclick = () => {
const [sc,i] = b.dataset.chk.split(":");
const a = arrOf(sc)[+i];
a.d = !a.d;
if(sc === "i"){ if(a.d) linkMedia(a); syncItemStatus(); }
saveNotes(); drawAll();
});
M.querySelectorAll("[data-edit]").forEach(e => e.onclick = () => {
const [s2,i] = e.dataset.edit.split(":"); editing = {s:s2, i:+i}; drawMain();
const t = $("#ed-note"); if(t){ t.focus(); t.setSelectionRange(t.value.length, t.value.length); }
});
const closeEdit = save => {
if(!editing) return;
if(save){ const t = $("#ed-note"); const v = t ? t.value.trim() : "";
if(v) arrOf(editing.s)[editing.i].t = v; }
editing = null; saveNotes(); drawAll();
};
M.querySelectorAll("[data-esave]").forEach(b => b.onclick = () => closeEdit(true));
M.querySelectorAll("[data-ecancel]").forEach(b => b.onclick = () => closeEdit(false));
M.querySelectorAll("[data-ep]").forEach(b => b.onclick = () => {
arrOf(editing.s)[editing.i].p = +b.dataset.ep; saveNotes();
M.querySelectorAll("[data-ep]").forEach(o => o.classList.toggle("on", +o.dataset.ep === +b.dataset.ep));
});
const T = $("#ed-note");
if(T) T.onkeydown = e => {
if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); closeEdit(true); }
if(e.key === "Escape"){ e.preventDefault(); closeEdit(false); }
};
const mvNote = (d, sc, i) => {
const arr = sc === "g" ? S.global : nt(item().id), j = i + d;
if(j < 0 || j >= arr.length) return;
[arr[i], arr[j]] = [arr[j], arr[i]];
saveNotes(); drawAll();
};
M.querySelectorAll("[data-mvu]").forEach(b => b.onclick = () => {
const [sc,i] = b.dataset.mvu.split(":"); mvNote(-1, sc, +i);
});
M.querySelectorAll("[data-mvd]").forEach(b => b.onclick = () => {
const [sc,i] = b.dataset.mvd.split(":"); mvNote(1, sc, +i);
});
}
function linkMedia(a){
const x = item(), o = st(x.id);
(x.mediaList||[]).forEach((m,j)=>{ if(m.id && a.t.indexOf(m.id) >= 0) o.vprep["m"+j] = true; });
}
const prepIdx = () => KAT().findIndex(k => (k.rolle||"") === "prep");
function syncItemStatus(){
const x = item(), arr = nt(x.id), o = st(x.id);
const pi = prepIdx();
const pr = pi < 0 ? [] : arr.filter(a=>a.p===pi);
if(pr.length) o.prep = pr.every(a=>a.d);
if(arr.length) o.played = arr.every(a=>a.d);
}
function addNote(scope){
const el = $("#in-"+scope), v = el.value.trim();
if(!v) return;
(scope === "g" ? S.global : nt(item().id)).push({t:v, p:prio});
el.value = ""; saveNotes(); drawAll();
const n = $("#in-"+scope); if(n) n.focus();
}
/* ================= Baukasten: Punkt-Editor ================= */
function segText(x){
return (x.segs||[]).map(sg=>{
const v = (sg.videos||[]).map(v=>`${v.n}=${v.min}`).join(", ");
return (sg.kind === "block" ? "Block " : "Redner ") + sg.min + (v ? ": " + v : "");
}).join("\n");
}
function parseSegs(txt){
const segs = []; let t = 0;
(txt||"").split("\n").forEach(line=>{
const m = /^\s*(redner|block)\s+([\d.]+)\s*(?::(.*))?$/i.exec(line.trim());
if(!m) return;
const min = parseFloat(m[2]) || 0;
const vids = (m[3]||"").split(",").map(p=>{
const q = /^\s*(\d+)\s*=\s*([\d.]+)\s*$/.exec(p);
return q ? {n:+q[1], min:parseFloat(q[2])} : null;
}).filter(Boolean);
segs.push({start:t, min, kind: m[1].toLowerCase()==="block"?"block":"talk", videos:vids});
t += min;
});
return {segs, gesamt: Math.round(t) || null};
}
function setPath(o, path, val){
const ks = path.split("."); let t = o;
ks.slice(0,-1).forEach(k => { if(typeof t[k] !== "object" || t[k] === null) t[k] = {}; t = t[k]; });
t[ks[ks.length-1]] = val;
}
function getPath(o, path){
return path.split(".").reduce((t,k) => (t && t[k] !== undefined) ? t[k] : "", o);
}
const FARBWAHL = [["grau","Grau – zurückhaltend"],["blau","Blau – neutral"],["gelb","Gelb – wichtig"],
["rot","Rot – kritisch"],["violett","Violett – Stichwort"],["gruen","Grün – Vorbereitung"]];
function drawEditor(){
let h = `<div class="sw" style="margin-bottom:12px;width:fit-content">
${[["punkt","Programmpunkt"],["schicht","Schicht & Besetzung"],["cfg","Einstellungen"]]
.map(([k,l])=>`<button data-bt="${k}" class="${bauTab===k?"on":""}">${l}</button>`).join("")}
</div>`;
if(bauTab === "punkt") h += edPunkt();
if(bauTab === "schicht") h += edSchicht();
if(bauTab === "cfg") h += edCfg();
h += `<div class="card"><div class="hd"><h3>DATEN</h3></div><div class="bd ed" style="display:flex;gap:7px;flex-wrap:wrap">
<button class="bt" id="pExp">Programm exportieren</button>
<label class="bt" style="display:inline-block">Programm importieren<input type="file" id="pImp" accept=".json" hidden></label>
<button class="bt dan" id="pReset">Auf Auslieferungsstand zurücksetzen</button>
</div></div>`;
$("#main").innerHTML = h;
bindEditor();
}
function edPunkt(){
const x = item();
return `<div class="card"><div class="hd"><h3>PROGRAMMPUNKT · GRUNDDATEN</h3></div><div class="bd ed">
<div class="grid">
<div class="fl"><label>UHRZEIT</label><input data-f="time" value="${esc(x.time)}" placeholder="15:30"></div>
<div class="fl"><label>NUMMER</label><input data-f="nr" value="${x.nr??""}" placeholder="26"></div>
<div class="fl"><label>DAUER (MIN.)</label><input data-f="dauer" value="${x.dauer??""}" placeholder="24"></div>
<div class="fl"><label>ART</label><select data-f="art">${ARTEN().map(a=>`<option ${a===(x.art||"")?"selected":""}>${esc(a)}</option>`).join("")}</select></div>
</div>
<div class="fl" style="margin-top:9px"><label>TITEL</label><input data-f="titel" value="${esc(x.titel)}"></div>
<div class="grid" style="margin-top:9px">
<div class="fl"><label>${esc(LBL("redner").toUpperCase())}</label><input data-f="redner" value="${esc(x.redner)}"></div>
<div class="fl"><label>ZUSATZ / HINWEIS</label><input data-f="hinweis" value="${esc(x.hinweis)}"></div>
</div>
<div class="fl" style="margin-top:9px"><label>FAKTEN-KURZFASSUNG (ERSCHEINT IN LISTE UND JETZT-ANSICHT)</label>
<input data-f="kurz" value="${esc(x.kurz)}" placeholder="Musik Nr. 12 · Video ankündigen"></div>
<div style="display:flex;gap:16px;margin-top:11px;font-size:13px;flex-wrap:wrap">
${useSync()?`<label style="display:flex;gap:6px;align-items:center"><input type="checkbox" data-f="sync" ${x.sync?"checked":""} style="width:auto"> SYNC (${esc(L2().name||"Fremdsprache")})</label>`:""}
${useStream()?`<label style="display:flex;gap:6px;align-items:center"><input type="checkbox" data-f="stream" ${x.stream?"checked":""} style="width:auto"> STREAM${ST2().quelle?" ("+esc(ST2().quelle)+")":""}</label>`:""}
</div>
</div></div>
<div class="card"><div class="bd" style="font-size:13px;color:var(--tx2)">
Medien, Zeitleiste und der offizielle Anweisungstext werden rechts im Reiter
<b>${esc(LBL("ablauf"))} bearbeiten</b> gepflegt, die Gliederung unter <b>Disposition bearbeiten</b>.
</div></div>`;
}
function edSchicht(){
return `<div class="card"><div class="hd"><h3>SCHICHT</h3></div><div class="bd ed">
<div class="grid">
<div class="fl"><label>BEZEICHNUNG</label><input data-sf="label" value="${esc(shift().label)}"></div>
<div class="fl"><label>${esc(LBL("vorsitz").toUpperCase())}</label><input data-sf="vorsitz" value="${esc(shift().vorsitz)}"></div>
</div>
<div class="fl" style="margin-top:9px"><label>${esc(LBL("motto").toUpperCase())} / UNTERTITEL</label><input data-sf="motto" value="${esc(shift().motto)}"></div>
<h4 style="margin:16px 0 8px">BESETZUNG</h4>
<table class="mt"><thead><tr><th style="width:44%">ROLLE / POSITION</th><th>NAME</th><th style="width:30px"></th></tr></thead><tbody>
${(shift().crew||[]).map((c,i)=>`<tr>
<td><input data-c="${i}" data-cf="rolle" value="${esc(c.rolle)}"></td>
<td><input data-c="${i}" data-cf="name" value="${esc(c.name)}" placeholder="Vorname Nachname"></td>
<td><button class="bt dan" data-cdel="${i}" style="padding:5px 7px">×</button></td></tr>`).join("")
|| `<tr><td colspan="3" class="empty" style="padding:6px 4px">Noch keine Positionen angelegt.</td></tr>`}
</tbody></table>
<div style="display:flex;gap:7px;margin-top:9px;flex-wrap:wrap">
<button class="bt" id="addCrew">+ Position</button>
${P.shifts.length>1?`<button class="bt" id="copyCrew">Besetzung der anderen Schicht übernehmen</button>`:""}
</div>
<div style="margin-top:14px;display:flex;gap:7px"><button class="bt dan" id="delShift">Schicht löschen</button></div>
</div></div>`;
}
function edCfg(){
const c = C();
const liste = (label, path, ph) => `<div class="fl" style="margin-top:9px"><label>${label}</label>
<textarea class="ta" data-list="${path}" rows="4" placeholder="${ph}">${esc((getPath(c,path)||[]).join("\n"))}</textarea></div>`;
return `
<div class="card"><div class="hd"><h3>VERANSTALTUNG UND ORT</h3></div><div class="bd ed">
<div class="grid">
<div class="fl"><label>KURZNAME DES TOOLS</label><input data-cp="titel" value="${esc(c.titel||"")}" placeholder="TD-Pult"></div>
<div class="fl"><label>VERANSTALTUNG</label><input data-cp="veranstaltung" value="${esc(c.veranstaltung||"")}" placeholder="Jahrestagung 2026"></div>
</div>
<div class="grid" style="margin-top:9px">
<div class="fl"><label>LOKAL · NAME</label><input data-cp="lokal.name" value="${esc(getPath(c,"lokal.name"))}" placeholder="Stadthalle"></div>
<div class="fl"><label>LOKAL · STADT</label><input data-cp="lokal.stadt" value="${esc(getPath(c,"lokal.stadt"))}" placeholder="Musterstadt"></div>
<div class="fl"><label>LOKAL · SPRACHE</label><input data-cp="lokal.sprache" value="${esc(getPath(c,"lokal.sprache"))}" placeholder="Deutsch"></div>
</div>
</div></div>
<div class="card"><div class="hd"><h3>ANGESCHLOSSENE SPRACHE</h3></div><div class="bd ed">
<label style="display:flex;gap:7px;align-items:center;font-size:14px">
<input type="checkbox" data-cb="lang2.aktiv" ${useSync()?"checked":""} style="width:auto">
Es ist ein weiterer Sprachsektor angeschlossen</label>
<div class="grid" style="margin-top:10px">
<div class="fl"><label>SPRACHE</label><input data-cp="lang2.name" value="${esc(getPath(c,"lang2.name"))}" placeholder="Englisch"></div>
<div class="fl"><label>KÜRZEL FÜR KENNZEICHEN</label><input data-cp="lang2.kurz" value="${esc(getPath(c,"lang2.kurz"))}" placeholder="EN"></div>
</div>
<div class="syn">Ohne angeschlossene Sprache verschwindet der Sync-Schritt bei den Medien,
das SYNC-Kennzeichen und das Häkchen im Programmpunkt.</div>
</div></div>
<div class="card"><div class="hd"><h3>STREAM / ÜBERTRAGUNG</h3></div><div class="bd ed">
<label style="display:flex;gap:7px;align-items:center;font-size:14px">
<input type="checkbox" data-cb="stream.aktiv" ${useStream()?"checked":""} style="width:auto">
Es werden Programmteile von außerhalb übertragen</label>
<div class="fl" style="margin-top:10px"><label>QUELLE / ORT</label>
<input data-cp="stream.quelle" value="${esc(getPath(c,"stream.quelle"))}" placeholder="Saal B"></div>
</div></div>
<div class="card"><div class="hd"><h3>BEZEICHNUNGEN</h3></div><div class="bd ed">
<div class="grid">
<div class="fl"><label>STATUS · VORBEREITET</label><input data-cp="status.prep" value="${esc(getPath(c,"status.prep"))}" placeholder="Medien & Cams vorbereitet"></div>
<div class="fl"><label>STATUS · ERLEDIGT</label><input data-cp="status.done" value="${esc(getPath(c,"status.done"))}" placeholder="Punkt erledigt"></div>
</div>
<div class="grid" style="margin-top:9px">
<div class="fl"><label>MEDIEN-SCHRITT 1</label><input data-cp="schritte.prep" value="${esc(getPath(c,"schritte.prep"))}" placeholder="vorbereitet"></div>
<div class="fl"><label>MEDIEN-SCHRITT 2</label><input data-cp="schritte.sync" value="${esc(getPath(c,"schritte.sync"))}" placeholder="sync"></div>
<div class="fl"><label>MEDIEN-SCHRITT 3</label><input data-cp="schritte.done" value="${esc(getPath(c,"schritte.done"))}" placeholder="abgespielt"></div>
</div>
<div class="grid" style="margin-top:9px">
<div class="fl"><label>REITER ABLAUFPLAN</label><input data-cp="bezeichnungen.ablauf" value="${esc(getPath(c,"bezeichnungen.ablauf"))}" placeholder="Ablaufplan"></div>
<div class="fl"><label>LEITUNG DER SCHICHT</label><input data-cp="bezeichnungen.vorsitz" value="${esc(getPath(c,"bezeichnungen.vorsitz"))}" placeholder="Vorsitz"></div>
<div class="fl"><label>MOTTO</label><input data-cp="bezeichnungen.motto" value="${esc(getPath(c,"bezeichnungen.motto"))}" placeholder="Tagesmotto"></div>
<div class="fl"><label>VORTRAGENDE</label><input data-cp="bezeichnungen.redner" value="${esc(getPath(c,"bezeichnungen.redner"))}" placeholder="Redner"></div>
</div>
<div class="syn">Die Begriffe erscheinen in Kopfzeile, Reitern und Formularen.</div>
</div></div>
<div class="card"><div class="hd"><h3>KATEGORIEN DER ANWEISUNGEN</h3></div><div class="bd ed">
<table class="mt"><thead><tr><th>NAME</th><th style="width:34%">DARSTELLUNG</th><th style="width:22%">SONDERROLLE</th><th style="width:66px"></th></tr></thead><tbody>
${(c.kategorien||[]).map((k,i)=>`<tr>
<td><input data-k="${i}" data-kf="name" value="${esc(k.name)}"></td>
<td><select data-k="${i}" data-kf="farbe">${FARBWAHL.map(([v,l])=>`<option value="${v}" ${v===k.farbe?"selected":""}>${l}</option>`).join("")}</select></td>
<td><select data-k="${i}" data-kf="rolle">
<option value="" ${!k.rolle?"selected":""}>—</option>
<option value="prep" ${k.rolle==="prep"?"selected":""}>Vorbereitung</option></select></td>
<td style="white-space:nowrap">
<button class="bt" data-kup="${i}" style="padding:5px 6px">▲</button>
<button class="bt dan" data-kdel="${i}" style="padding:5px 7px">×</button></td></tr>`).join("")}
</tbody></table>
<button class="bt" id="addKat" style="margin-top:9px">+ Kategorie</button>
<div class="syn">Die Sonderrolle <b>Vorbereitung</b> koppelt eine Kategorie an den Status
„vorbereitet“: sind alle Anweisungen dieser Kategorie abgehakt, gilt der Punkt als vorbereitet.
Nur eine Kategorie sollte diese Rolle tragen.</div>
</div></div>
<div class="card"><div class="hd"><h3>BAUSTEINE FÜR ANWEISUNGEN</h3></div><div class="bd ed">
${(c.bausteine||[]).map((g,i)=>`<div style="margin-bottom:10px">
<div style="display:flex;gap:7px;align-items:center">
<input data-g="${i}" data-gf="gruppe" value="${esc(g.gruppe)}" style="width:200px" placeholder="Gruppenname">
<button class="bt dan" data-gdel="${i}" style="padding:5px 8px">Gruppe löschen</button>
</div>
<textarea class="ta" data-g="${i}" data-gf="werte" rows="2" style="margin-top:5px"
placeholder="Werte mit Komma trennen">${esc((g.werte||[]).join(", "))}</textarea>
</div>`).join("")}
<button class="bt" id="addGrp">+ Gruppe</button>
<div class="syn">Diese Werte erscheinen als klickbare Chips unter dem Eingabefeld —
hier gehören eure vereinbarten Bezeichnungen und Stichwörter hinein.</div>
</div></div>
<div class="card"><div class="hd"><h3>AUSWAHLLISTEN</h3></div><div class="bd ed">
${liste("ARTEN VON PROGRAMMPUNKTEN", "arten", "Vortrag&#10;Musik&#10;Pause")}
${liste("MEDIENARTEN", "medienarten", "Video&#10;Musik&#10;Bild")}
${liste("ZUSATZAUFGABEN BEIM ANLEGEN DER VORBEREITUNG", "prepExtra", "Kameras prüfen&#10;Ton prüfen")}
<div class="syn">Eine Zeile pro Eintrag.</div>
</div></div>
<div class="card"><div class="hd"><h3>AUTOMATISCHE HINWEISE</h3></div><div class="bd ed">
<table class="mt"><thead><tr><th style="width:26%">WENN TITEL ENTHÄLT</th><th>HINWEISTEXT</th><th style="width:30px"></th></tr></thead><tbody>
${(c.regeln||[]).map((r,i)=>`<tr>
<td><input data-r2="${i}" data-rf="suche" value="${esc(r.suche)}" placeholder="Interview"></td>
<td><input data-r2="${i}" data-rf="text" value="${esc(r.text)}" placeholder="Hier gilt die Ausnahme …"></td>
<td><button class="bt dan" data-rdel="${i}" style="padding:5px 7px">×</button></td></tr>`).join("")
|| `<tr><td colspan="3" class="empty" style="padding:6px 4px">Keine Hinweise angelegt.</td></tr>`}
</tbody></table>
<button class="bt" id="addRegel" style="margin-top:9px">+ Hinweis</button>
<div class="syn">Der Hinweis erscheint gelb in der Jetzt-Ansicht, sobald Titel, Kurzfassung
oder Art des Programmpunkts den Suchbegriff enthalten.</div>
</div></div>
<div class="card"><div class="hd"><h3>EINSTELLUNGEN ZURÜCKSETZEN</h3></div><div class="bd ed">
<button class="bt dan" id="cfgReset">Nur die Einstellungen auf Auslieferungsstand</button>
</div></div>`;
}
function bindEditor(){
const x = item(), M = $("#main");
M.querySelectorAll("[data-bt]").forEach(b => b.onclick = () => { bauTab = b.dataset.bt; drawEditor(); });
/* --- Schicht --- */
M.querySelectorAll("[data-sf]").forEach(el => el.oninput = () => {
shift()[el.dataset.sf] = el.value; saveProg(); drawHead(); drawTabs(); drawSide();
});
M.querySelectorAll("[data-cf]").forEach(el => el.oninput = () => {
shift().crew[+el.dataset.c][el.dataset.cf] = el.value; saveProg();
});
M.querySelectorAll("[data-cdel]").forEach(b => b.onclick = () => {
shift().crew.splice(+b.dataset.cdel,1); saveProg(); drawEditor();
});
const AC = $("#addCrew");
if(AC) AC.onclick = () => { (shift().crew = shift().crew || []).push({rolle:"", name:""}); saveProg(); drawEditor(); };
const CC = $("#copyCrew");
if(CC) CC.onclick = () => {
const other = P.shifts[(shiftIdx+1) % P.shifts.length];
if(!confirm("Besetzung aus „" + other.label + "“ übernehmen? Die hier eingetragenen Namen werden ersetzt.")) return;
shift().crew = clone(other.crew || []); saveProg(); drawEditor();
};
const DS = $("#delShift");
if(DS) DS.onclick = () => {
if(P.shifts.length < 2) return alert("Die letzte Schicht kann nicht gelöscht werden.");
if(!confirm("Schicht „"+shift().label+"“ mit allen Punkten löschen?")) return;
P.shifts.splice(shiftIdx,1); shiftIdx = 0; curIdx = 0; saveProg(); drawAll();
};
/* --- Programmpunkt --- */
bindFields(M);
/* --- Einstellungen --- */
M.querySelectorAll("[data-cp]").forEach(el => el.oninput = () => {
setPath(C(), el.dataset.cp, el.value); saveProg(); drawHead(); drawSide(); drawRight();
});
M.querySelectorAll("[data-cb]").forEach(el => el.onchange = () => {
setPath(C(), el.dataset.cb, el.checked); saveProg(); drawHead(); drawRight(); drawSide();
});
M.querySelectorAll("[data-list]").forEach(el => el.oninput = () => {
setPath(C(), el.dataset.list, el.value.split("\n").map(t=>t.trim()).filter(Boolean)); saveProg();
});
M.querySelectorAll("[data-kf]").forEach(el => {
const h = () => { C().kategorien[+el.dataset.k][el.dataset.kf] = el.value; saveProg(); drawSide(); };
el.oninput = h; if(el.tagName === "SELECT") el.onchange = h;
});
M.querySelectorAll("[data-kdel]").forEach(b => b.onclick = () => {
if(C().kategorien.length < 2) return alert("Mindestens eine Kategorie muss bleiben.");
C().kategorien.splice(+b.dataset.kdel,1); saveProg(); drawEditor();
});
M.querySelectorAll("[data-kup]").forEach(b => b.onclick = () => {
const i = +b.dataset.kup, a = C().kategorien;
if(i === 0) return;
[a[i-1], a[i]] = [a[i], a[i-1]]; saveProg(); drawEditor();
});
const AK = $("#addKat");
if(AK) AK.onclick = () => { (C().kategorien = C().kategorien || []).push({name:"Neu", farbe:"grau", rolle:""}); saveProg(); drawEditor(); };
M.querySelectorAll("[data-gf]").forEach(el => el.oninput = () => {
const g = C().bausteine[+el.dataset.g];
if(el.dataset.gf === "werte") g.werte = el.value.split(",").map(t=>t.trim()).filter(Boolean);
else g.gruppe = el.value;
saveProg();
});
M.querySelectorAll("[data-gdel]").forEach(b => b.onclick = () => {
C().bausteine.splice(+b.dataset.gdel,1); saveProg(); drawEditor();
});
const AG = $("#addGrp");
if(AG) AG.onclick = () => { (C().bausteine = C().bausteine || []).push({gruppe:"Neue Gruppe", werte:[]}); saveProg(); drawEditor(); };
M.querySelectorAll("[data-rf]").forEach(el => el.oninput = () => {
C().regeln[+el.dataset.r2][el.dataset.rf] = el.value; saveProg();
});
M.querySelectorAll("[data-rdel]").forEach(b => b.onclick = () => {
C().regeln.splice(+b.dataset.rdel,1); saveProg(); drawEditor();
});
const AR = $("#addRegel");
if(AR) AR.onclick = () => { (C().regeln = C().regeln || []).push({suche:"", text:""}); saveProg(); drawEditor(); };
const CR = $("#cfgReset");
if(CR) CR.onclick = () => {
if(!confirm("Alle Einstellungen auf den Auslieferungsstand zurücksetzen? Programm und Anweisungen bleiben.")) return;
P.cfg = clone(SEED.cfg || {}); saveProg(); drawAll();
};
/* --- Dateien --- */
$("#pExp").onclick = () => dl(P, "td-programm");
$("#pImp").onchange = e => rd(e, o => { if(!o.shifts) throw 0; P = o; shiftIdx = 0; curIdx = 0; saveProg(); drawAll(); });
$("#pReset").onclick = () => {
if(!confirm("Alle Programmänderungen verwerfen und den ursprünglichen Stand wiederherstellen? Anweisungen bleiben erhalten.")) return;
P = clone(SEED); shiftIdx = 0; curIdx = 0; saveProg(); drawAll();
};
}
function bindFields(root){
const x = item();
root.querySelectorAll("[data-f]").forEach(el => {
const f = el.dataset.f;
el.oninput = () => {
if(f === "sync" || f === "stream") x[f] = el.checked;
else if(f === "nr" || f === "dauer") x[f] = el.value.trim() === "" ? null : (parseInt(el.value,10) || null);
else if(f === "volltext") x.volltext = el.value.split("\n").map(t=>t.trim()).filter(Boolean);
else if(f === "segtext"){ const r = parseSegs(el.value); x.segs = r.segs; x.gesamt = r.gesamt; }
else x[f] = el.value;
saveProg(); drawSide();
};
if(el.tagName === "SELECT") el.onchange = el.oninput;
});
}
function bindMedia(root){
const x = item();
root.querySelectorAll("[data-mf]").forEach(el => {
el.oninput = () => {
const m = x.mediaList[+el.dataset.m], f = el.dataset.mf;
if(f === "sync" || f === "stream") m[f] = el.checked;
else if(f === "n") m.n = el.value.trim() === "" ? null : (parseInt(el.value,10) || null);
else m[f] = el.value;
saveProg(); drawSide();
};
if(el.tagName === "SELECT") el.onchange = el.oninput;
});
root.querySelectorAll("[data-mdel]").forEach(b => b.onclick = () => {
x.mediaList.splice(+b.dataset.mdel,1); saveProg(); drawRight();
});
const A = root.querySelector("#addMed");
if(A) A.onclick = () => {
(x.mediaList = x.mediaList || []).push({kind:"Video", n:((x.mediaList||[]).filter(m=>m.kind==="Video").length+1), id:"", len:"", sync:false, stream:false});
saveProg(); drawRight();
};
}
/* ---------- Baukasten: Editoren in der rechten Spalte ---------- */
function dispText(d){
if(!d) return "";
return (d.body||[]).map(b=>{
if(b.k === "h") return "# " + b.text;
if(b.k === "cue") return "> " + b.text;
if(b.k === "lies") return "! " + b.text;
return " ".repeat(b.lvl||0) + b.text;
}).join("\n");
}
function parseDisp(txt){
const body = [];
(txt||"").split("\n").forEach(line=>{
if(!line.trim()) return;
const t = line.trim();
if(t.startsWith("#")) body.push({k:"h", lvl:0, text:t.replace(/^#\s*/,"")});
else if(t.startsWith(">")) body.push({k:"cue", lvl:0, text:t.replace(/^>\s*/,"")});
else if(t.startsWith("!")) body.push({k:"lies", lvl:0, text:t.replace(/^!\s*/,"")});
else body.push({k:"p", lvl: Math.floor((line.match(/^ */)[0].length)/2), text:t});
});
return body;
}
function drawRightEditor(){
if(rTab === "co13") return drawCo13Editor();
if(rTab === "allg") return drawAllgEditor();
const x = item();
$("#rbody").innerHTML = `
<div style="font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700;margin-bottom:10px">
DISPOSITION BEARBEITEN · ${esc(x.titel || x.time)}</div>
${x.dispo ? `
<div class="fl" style="margin-bottom:10px"><label>HINWEIS (${esc(LBL("redner").toUpperCase())})</label>
<textarea class="ta" id="dHint" rows="2">${esc(x.dispo.hinweis||"")}</textarea></div>
<textarea class="ta mono" id="dBody" rows="26" style="font-size:12.5px">${esc(dispText(x.dispo))}</textarea>
<div class="syn">
<code># ÜBERSCHRIFT (5 Min.)</code> Abschnitt · <code>&gt; [Das Video wird abgespielt (3:33)]</code> Video-Cue ·
<code>! [Zitat vorlesen]</code> Lesung / Zitat<br>
Alles andere ist ein Gliederungspunkt; <b>zwei Leerzeichen</b> am Zeilenanfang je Ebene.
</div>
<button class="bt dan" id="dDel" style="margin-top:10px">Disposition entfernen</button>`
: `<div class="empty">Für diesen Punkt ist keine Disposition hinterlegt.</div>
<button class="bt pri" id="dNew" style="margin-top:10px">+ Disposition anlegen</button>`}`;
const N = $("#dNew");
if(N) N.onclick = () => { x.dispo = {hinweis:"", body:[]}; saveProg(); drawRight(); };
const D = $("#dDel");
if(D) D.onclick = () => { if(confirm("Disposition entfernen?")){ x.dispo = null; saveProg(); drawRight(); } };
const H = $("#dHint");
if(H) H.oninput = () => { x.dispo.hinweis = H.value; saveProg(); };
const B = $("#dBody");
if(B) B.oninput = () => { x.dispo.body = parseDisp(B.value); saveProg(); };
}
function drawCo13Editor(){
const x = item();
$("#rbody").innerHTML = `
<div style="font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700;margin-bottom:10px">
${esc(LBL("ablauf").toUpperCase())} BEARBEITEN · ${esc(x.time)} · ${esc(x.titel || x.kurz || "")}</div>
<div class="card"><div class="hd"><h3>MEDIEN</h3></div><div class="bd ed">
<table class="mt"><thead><tr>
<th style="width:104px">ART</th><th style="width:46px">NR.</th><th>DATEI-ID</th><th style="width:96px">LÄNGE</th>
<th style="width:44px">SYNC</th><th style="width:54px">STREAM</th><th style="width:30px"></th></tr></thead><tbody>
${(x.mediaList||[]).map((m,i)=>`<tr>
<td><select data-m="${i}" data-mf="kind">${MKIND().map(k=>`<option ${k===m.kind?"selected":""}>${esc(k)}</option>`).join("")}</select></td>
<td><input data-m="${i}" data-mf="n" value="${m.n??""}"></td>
<td><input class="mono" data-m="${i}" data-mf="id" value="${esc(m.id)}"></td>
<td><input data-m="${i}" data-mf="len" value="${esc(m.len)}" placeholder="3:33 Min."></td>
<td style="text-align:center"><input type="checkbox" data-m="${i}" data-mf="sync" ${m.sync?"checked":""} style="width:auto"></td>
<td style="text-align:center"><input type="checkbox" data-m="${i}" data-mf="stream" ${m.stream?"checked":""} style="width:auto"></td>
<td><button class="bt dan" data-mdel="${i}" style="padding:5px 7px">×</button></td></tr>`).join("")
|| `<tr><td colspan="7" class="empty" style="padding:6px 4px">Noch keine Medien hinterlegt.</td></tr>`}
</tbody></table>
<button class="bt" id="addMed" style="margin-top:9px">+ Medium</button>
</div></div>
<div class="card"><div class="hd"><h3>ZEITLEISTE</h3></div><div class="bd ed">
<textarea class="ta mono" data-f="segtext" rows="${Math.max(3,(x.segs||[]).length+1)}">${esc(segText(x))}</textarea>
<div class="syn">Eine Zeile pro Abschnitt, in der Reihenfolge des Vortrags.<br>
<code>Redner 2</code> = 2 Minuten nur Redner · <code>Block 8: 1=1, 2=6</code> = 8-Minuten-Abschnitt,
darin Video 1 (ca. 1 Min.) und Video 2 (ca. 6 Min.).<br>
Die Nummern verweisen auf die Medien-Nummern oben und ergeben die geschätzten Video-Uhrzeiten.</div>
</div></div>
<div class="card"><div class="hd"><h3>OFFIZIELLER ANWEISUNGSTEXT</h3></div><div class="bd ed">
<textarea class="ta" data-f="volltext" rows="10" placeholder="Ein Absatz pro Zeile.">${esc((x.volltext||[]).join("\n"))}</textarea>
<div class="syn">Jede Zeile ist ein eigener Absatz. Erscheint im Live-Betrieb im Reiter ${esc(LBL("ablauf"))} und beim Aufklappen in der Ablaufliste.</div>
</div></div>`;
bindFields($("#rbody"));
bindMedia($("#rbody"));
}
function drawAllgEditor(){
if(!P.general) P.general = {body:[]};
$("#rbody").innerHTML = `
<div style="font-size:11px;letter-spacing:.14em;color:var(--tx3);font-weight:700;margin-bottom:10px">
ALLGEMEINE ANWEISUNGEN BEARBEITEN</div>
<textarea class="ta mono" id="aBody" rows="30" style="font-size:12.5px">${esc(dispText(P.general))}</textarea>
<div class="syn"><code># ÜBERSCHRIFT</code> Abschnitt · alles andere ist eine Zeile ·
<b>zwei Leerzeichen</b> am Zeilenanfang rücken ein.</div>
<button class="bt dan" id="aReset" style="margin-top:10px">Auslieferungstext wiederherstellen</button>`;
const B = $("#aBody");
B.oninput = () => { P.general.body = parseDisp(B.value); saveProg(); };
$("#aReset").onclick = () => {
if(!confirm("Allgemeine Anweisungen auf den Auslieferungstext zurücksetzen?")) return;
P.general = clone(SEED.general || {body:[]}); saveProg(); drawRight();
};
}
/* ================= Dateien ================= */
function dl(obj, name){
const b = new Blob([JSON.stringify(obj,null,1)], {type:"application/json"});
const a = document.createElement("a");
a.href = URL.createObjectURL(b);
a.download = name + "-" + new Date().toISOString().slice(0,10) + ".json";
a.click();
}
function rd(e, fn){
const f = e.target.files[0]; if(!f) return;
const r = new FileReader();
r.onload = () => { try{ fn(JSON.parse(r.result)); }catch(x){ alert("Datei nicht lesbar."); } };
r.readAsText(f); e.target.value = "";
}
$("#menuBtn").onclick = e => {
e.stopPropagation();
const m = $("#menu"); m.style.display = m.style.display === "none" ? "block" : "none";
};
document.addEventListener("click", () => { const m = $("#menu"); if(m) m.style.display = "none"; });
$("#menu").onclick = e => e.stopPropagation();
$("#menu").querySelectorAll("[data-fx]").forEach(el => {
const fx = el.dataset.fx;
if(fx === "ne") el.onclick = () => dl(S, "td-anweisungen");
if(fx === "pe") el.onclick = () => dl(P, "td-programm");
if(fx === "ni") el.onchange = e => rd(e, o => { if(!o.notes) throw 0; S = o; saveNotes(); drawAll(); });
if(fx === "pi") el.onchange = e => rd(e, o => { if(!o.shifts) throw 0; P = o; shiftIdx = 0; curIdx = 0; saveProg(); drawAll(); });
});
/* ================= Steuerung ================= */
$("#rtabs").querySelectorAll("button").forEach(b => b.onclick = () => { rTab = b.dataset.r; drawRight(); });
$("#dsw").querySelectorAll("button").forEach(b => b.onclick = () => { dispMode = b.dataset.d; drawRight(); });
$("#prevBtn").onclick = () => { curIdx = Math.max(0, curIdx-1); drawAll(); };
$("#nextBtn").onclick = () => { curIdx = Math.min(items().length-1, curIdx+1); drawAll(); };
$("#nowBtn").onclick = () => { curIdx = nearestIdx(); drawAll(); };
$("#modeBtn").onclick = () => { editMode = !editMode; syncBtns(); drawAll(); };
$("#bauBtn").onclick = () => { bau = !bau; if(bau) bauTab = "punkt"; syncBtns(); drawAll(); };
document.addEventListener("keydown", e => {
if(/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) return;
if(e.key === "ArrowDown" || e.key === "ArrowRight"){ $("#nextBtn").click(); e.preventDefault(); }
if(e.key === "ArrowUp" || e.key === "ArrowLeft"){ $("#prevBtn").click(); e.preventDefault(); }
if((e.key === "e" || e.key === "E") && !bau){ $("#modeBtn").click(); }
});
function drawAll(){ drawHead(); drawTabs(); drawSide(); drawMain(); drawRight(); }
curIdx = nearestIdx();
drawAll(); syncBtns();
setInterval(drawHead, 1000);
setInterval(()=>{ if(!bau && $("#main").querySelector(".nv")) drawMain(); }, 30000);
</script>
</body>
</html>

Powered by TurnKey Linux.