|
|
<!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 => ({"&":"&","<":"<",">":">",'"':"""}[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. ${x.nr}</span>`:""}${x.sync?"<span>SYNC</span>":""}${x.stream?"<span>STREAM</span>":""}${n?`<span>${n} 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">
|
|
|
← → Punkt wechseln · E Modus<br>
|
|
|
Enter 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 & 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 Musik Pause")}
|
|
|
${liste("MEDIENARTEN", "medienarten", "Video Musik Bild")}
|
|
|
${liste("ZUSATZAUFGABEN BEIM ANLEGEN DER VORBEREITUNG", "prepExtra", "Kameras prüfen 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>> [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>
|