BruceBanners commited on
Commit
718cb25
·
verified ·
1 Parent(s): 5273c56

PLease make my code better and visually more stunning

Browse files

<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Split-flap Admin – Canvas</title>
<style>
html,body{height:100%;margin:0;background:#0f1115;color:#e5e7eb;font-family:system-ui,Segoe UI,Roboto,Inter}
.wrap{display:grid;grid-template-columns:360px 1fr;gap:12px;height:100vh;padding:12px;box-sizing:border-box}
.panel{border:1px solid #1f2330;border-radius:10px;background:#0b0d12;padding:12px;overflow:auto}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
input,select,button,textarea{background:#1f2937;color:#fff;border:1px solid #374151;border-radius:8px;padding:8px}
textarea{width:100%}
button{cursor:pointer}
.small{width:90px} .mid{width:160px} .wide{width:100%}
.badge{padding:4px 8px;border-radius:6px;background:#1b2432;border:1px solid #2a3344}
.mono{font-family:ui-monospace,Consolas,monospace}
.gridwrap{position:relative;background:#0b0f16;border:1px solid #1c2433;border-radius:10px;aspect-ratio: var(--cols) / var(--rows)}
.grid{position:absolute;inset:0;background-size:calc(100%/var(--cols)) calc(100%/var(--rows));background-image:linear-gradient(#1a2333 1px,transparent 1px),linear-gradient(90deg,#1a2333 1px,transparent 1px)}
.tile{position:absolute;border:1px solid #2a3344;border-radius:8px;background:#101827;box-shadow:0 2px 8px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;text-align:center;padding:6px;user-select:none;overflow:hidden}
.tile.sel{outline:2px solid #6daaff}
.handle{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;background:#6daaff;border-radius:3px;cursor:nwse-resize}
.list{white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;font-size:12px;background:#0e131b;border:1px solid #223;border-radius:8px;padding:8px}
</style>
</head>
<body>
<div class="wrap">
<!-- venstre -->
<div class="panel">
<h2>Oppsett <span id="status" class="badge">idle</span></h2>
<div class="row">
<label>API</label><input id="apiUrl" class="mid" value="/cb/api.php">
<label>Room</label><input id="room" class="small" value="hallA">
<label>Token</label><input id="token" class="mid" value="BYTT-DETTE">
<button id="ping">Ping</button>
<button id="clear">Clear</button>
<button id="pushScene">Push nå</button>
</div>
<div class="row">
<label>Kolonner</label><input id="cols" class="small" type="number" value="20" min="4" max="80">
<label>Rader</label><input id="rows" class="small" type="number" value="10" min="4" max="40">
</div>

<h3>Ikoner</h3>
<div class="row">
<label>rail</label><input id="icoRail" class="small" value="🚆">
<label>bus</label><input id="icoBus" class="small" value="🚌">
<label>tram</label><input id="icoTram" class="small" value="🚊">
<label>metro</label><input id="icoMetro" class="small" value="🚇">
<label>water</label><input id="icoWater" class="small" value="🛥️">
<label>air</label><input id="icoAir" class="small" value="✈️">
<label>annet</label><input id="icoOther" class="small" value="•">
</div>

<h3>Tiles</h3>
<div class="row">
<button id="addClock">Ny klokke</button>
<button id="addEntur">Ny Entur-liste</button>
<button id="addText">Ny tekst</button>
<button id="dupTile">Dupliser</button>
<button id="delTile">Slett</button>
</div>

<h3>Egenskaper</h3>
<div id="props" class="row" style="align-items:flex-start">
<div class="wide">
<div class="row">
<label>ID</label><input id="p_id" class="mid" readonly>
<label>Type</label><input id="p_type" class="mid" readonly>
</div>
<div class="row">
<label>X</label><input id="p_x" class="small" type="number">
<label>Y</label><input id="p_y" class="small" type="number">
<label>W</label><input id="p_w" class="small" type="number">
<label>H</label><input id="p_h" class="small" type="number">
</div>
<div id="typeClock" style="display:none">
<div class="row">
<label>Format</label>
<select id="clk_fmt">
<option value="HH:mm">HH:mm</option>
<option value="HH:mm:ss">HH:mm:ss</option>
<option value="dd.MM HH:mm">dd.MM HH:mm</option>
<option value="dd.MM">dd.MM</option>
</select>
<label>Farge</label><input id="clk_color" class="small" value="#ffffff">
<label>Oppdater ms</label><input id="clk_ms" class="small" type="number" value="1000" min="200">
</div>
</div>
<div id="typeEntur" style="display:none">
<div class="row">
<label>StopPlace</label><input id="en_stop" class="mid" value="NSR:StopPlace:58287">
<label>Modus</label>
<select id="en_mode"><option value="departures">Fra</option><option value="arrivals">Til</option></select>
</div>
<div class="row">
<label>Limit</label><input id="en_limit" class="small" type="number" value="6" min="1" max="20">
<label>Hent hver s</label><input id="en_fetch" class="small" type="number" value="30" min="5">
<label>Linjer</label><input id="en_lines" class="small" type="number" value="6" min="1" max="20">
</div>
<div class="row">
<label>Vy</label><input id="en_vy" type="checkbox" checked>
<label>SJ</label><input id="en_sj" type="checkbox" checked>
<label>Flytoget</label><input id="en_fly" type="checkbox" checked>
<label>Forsinket</label><input id="en_onlydel" type="checkbox">
<label>Kansellert</label><input id="en_onlycan" type="checkbox">
</div>
<div class="row">
<label>Template per rad</label>
<input id="en_tpl" class="wide mono" value="[{icon}] {time} {line|cut=6} {dest|cutEnd=12} Sp{quay|pad=2} {status} {delay}">
</div>
<div class="row">
<label>Farge normal</label><input id="en_c_norm" class="small" value="#ffffff">
<label>Farge forsinket</label><input id="en_c_del" class="small" value="#ffe66d">
<label>Farge kansellert</label><input id="en_c_can" class="small" value="#ff5c5c">
</div>
<div class="row">
<button id="en_fetch_now">Hent nå</button>
<button id="en_preview">Forhåndsvis</button>
</div>
</div>
<div id="typeText" style="display:none">
<div class="row">
<label>Tekst</label><input id="tx_text" class="wide" value="Velkommen">
</div>
<div class="row">
<label>Farge</label><input id="tx_color" class="small" value="#6daaff">
</div>
</div>
</div>
</div>

<h3>Scene</h3>
<div class="row">
<label>Auto-push s</label><input id="scene_push" class="small" type="number" value="10" min="3">
<button id="startAuto">Start auto</button>
<button id="stopAuto">Stopp</button>
</div>

<h3>Lagring</h3>
<div class="row">
<button id="save">Lagre</button>
<button id="load">Last</button>
<button id="export">Eksporter</button>
<input id="importFile" type="file" accept="application/json">
</div>
</div>

<!-- høyre -->
<div class="panel">
<div class="row">
<strong>Canvas</strong>
<span>Kol × Rad:</span><span id="dim" class="badge">20 × 10</span>
</div>
<div id="canvas" class="gridwrap" style="--cols:20;--rows:10">
<div id="grid" class="grid" style="--cols:20;--rows:10"></div>
</div>
<h3>Logg</h3>
<div id="log" class="list"></div>
</div>
</div>

<script>
const S=id=>document.getElementById(id);
const statusEl=S('status'); const logEl=S('log');
function setStatus(s){ statusEl.textContent=s; }
function log(m,d){ const t=new Date().toLocaleTimeString(); const line=`[${t}] ${m}${d? " "+JSON.stringify(d):""}`; logEl.textContent=line+"\n"+logEl.textContent; console.log(m,d??""); }
window.addEventListener('error',e=>{log('JS',{m:e.message,src:e.filename,l:e.lineno});});

const ENTUR_URL = 'https://api.entur.io/journey-planner/v3/graphql';
// ved CORS: const ENTUR_URL = '/cb/proxy_entur.php';

const state = {
cols: parseInt(S('cols').value,10),
rows: parseInt(S('rows').value,10),
tiles: [],
sel: null,
autoPushTimer: null
};

function uid(){ return Math.random().toString(36).slice(2,8); }
function snap(v, max){ v=Math.max(0, Math.min(v, max)); return v; }
function updateGrid(){
const g = S('grid'); const wrap = S('canvas');
g.style.setProperty('--cols', state.cols);
g.style.setProperty('--rows', state.rows);
wrap.style.setProperty('--cols', state.cols);
wrap.style.setProperty('--rows', state.rows);
S('dim').textContent = `${state.cols} × ${state.rows}`;
}
S('cols').oninput=()=>{ state.cols=parseInt(S('cols').value,10)||20; updateGrid(); renderTiles(); drawTileContent(); };
S('rows').oninput=()=>{ state.rows=parseInt(S('rows').value,10)||10; updateGrid(); renderTiles(); drawTileContent(); };

function iconMapFor(mode){
const m=(mode||'').toLowerCase();
if(m==='rail') return S('icoRail').value||'🚆';
if(m==='bus') return S('icoBus').value||'🚌';
if(m==='tram') return S('icoTram').value||'🚊';
if(m==='metro') return S('icoMetro').value||'🚇';
if(m==='water') return S('icoWater').value||'🛥️';
if(m==='air') return S('icoAir').value||'✈️';
return S('icoOther').value||'•';
}

/* Canvas */
const canvas=S('canvas');
function cellToRect(x,y,w,h){
const r=canvas.getBoundingClientRect();
return {
left: (x/state.cols)*r.width,
top: (y/state.rows)*r.height,
width: (w/state.cols)*r.width,
height: (h/state.rows)*r.height
};
}
function rectToCell(left,top,width,height){
const r=canvas.getBoundingClientRect();
return {
x: Math.round(left / r.width * state.cols),
y: Math.round(top / r.height * state.rows),
w: Math.max(1, Math.round(width / r.width * state.cols)),
h: Math.max(1, Math.round(height/ r.height

Files changed (1) hide show
  1. index.html +0 -583
index.html CHANGED
@@ -1,583 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="no">
3
- <head>
4
- <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>Split-flap Admin – Canvas</title>
6
- <style>
7
- html,body{height:100%;margin:0;background:#0f1115;color:#e5e7eb;font-family:system-ui,Segoe UI,Roboto,Inter}
8
- .wrap{display:grid;grid-template-columns:360px 1fr;gap:12px;height:100vh;padding:12px;box-sizing:border-box}
9
- .panel{border:1px solid #1f2330;border-radius:10px;background:#0b0d12;padding:12px;overflow:auto}
10
- .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
11
- input,select,button,textarea{background:#1f2937;color:#fff;border:1px solid #374151;border-radius:8px;padding:8px}
12
- textarea{width:100%}
13
- button{cursor:pointer}
14
- .small{width:90px} .mid{width:160px} .wide{width:100%}
15
- .badge{padding:4px 8px;border-radius:6px;background:#1b2432;border:1px solid #2a3344}
16
- .mono{font-family:ui-monospace,Consolas,monospace}
17
- .gridwrap{position:relative;background:#0b0f16;border:1px solid #1c2433;border-radius:10px;aspect-ratio: var(--cols) / var(--rows)}
18
- .grid{position:absolute;inset:0;background-size:calc(100%/var(--cols)) calc(100%/var(--rows));background-image:linear-gradient(#1a2333 1px,transparent 1px),linear-gradient(90deg,#1a2333 1px,transparent 1px)}
19
- .tile{position:absolute;border:1px solid #2a3344;border-radius:8px;background:#101827;box-shadow:0 2px 8px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;text-align:center;padding:6px;user-select:none;overflow:hidden}
20
- .tile.sel{outline:2px solid #6daaff}
21
- .handle{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;background:#6daaff;border-radius:3px;cursor:nwse-resize}
22
- .list{white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;font-size:12px;background:#0e131b;border:1px solid #223;border-radius:8px;padding:8px}
23
- </style>
24
- </head>
25
- <body>
26
- <div class="wrap">
27
- <!-- venstre -->
28
- <div class="panel">
29
- <h2>Oppsett <span id="status" class="badge">idle</span></h2>
30
- <div class="row">
31
- <label>API</label><input id="apiUrl" class="mid" value="/cb/api.php">
32
- <label>Room</label><input id="room" class="small" value="hallA">
33
- <label>Token</label><input id="token" class="mid" value="BYTT-DETTE">
34
- <button id="ping">Ping</button>
35
- <button id="clear">Clear</button>
36
- <button id="pushScene">Push nå</button>
37
- </div>
38
- <div class="row">
39
- <label>Kolonner</label><input id="cols" class="small" type="number" value="20" min="4" max="80">
40
- <label>Rader</label><input id="rows" class="small" type="number" value="10" min="4" max="40">
41
- </div>
42
-
43
- <h3>Ikoner</h3>
44
- <div class="row">
45
- <label>rail</label><input id="icoRail" class="small" value="🚆">
46
- <label>bus</label><input id="icoBus" class="small" value="🚌">
47
- <label>tram</label><input id="icoTram" class="small" value="🚊">
48
- <label>metro</label><input id="icoMetro" class="small" value="🚇">
49
- <label>water</label><input id="icoWater" class="small" value="🛥️">
50
- <label>air</label><input id="icoAir" class="small" value="✈️">
51
- <label>annet</label><input id="icoOther" class="small" value="•">
52
- </div>
53
-
54
- <h3>Tiles</h3>
55
- <div class="row">
56
- <button id="addClock">Ny klokke</button>
57
- <button id="addEntur">Ny Entur-liste</button>
58
- <button id="addText">Ny tekst</button>
59
- <button id="dupTile">Dupliser</button>
60
- <button id="delTile">Slett</button>
61
- </div>
62
-
63
- <h3>Egenskaper</h3>
64
- <div id="props" class="row" style="align-items:flex-start">
65
- <div class="wide">
66
- <div class="row">
67
- <label>ID</label><input id="p_id" class="mid" readonly>
68
- <label>Type</label><input id="p_type" class="mid" readonly>
69
- </div>
70
- <div class="row">
71
- <label>X</label><input id="p_x" class="small" type="number">
72
- <label>Y</label><input id="p_y" class="small" type="number">
73
- <label>W</label><input id="p_w" class="small" type="number">
74
- <label>H</label><input id="p_h" class="small" type="number">
75
- </div>
76
- <div id="typeClock" style="display:none">
77
- <div class="row">
78
- <label>Format</label>
79
- <select id="clk_fmt">
80
- <option value="HH:mm">HH:mm</option>
81
- <option value="HH:mm:ss">HH:mm:ss</option>
82
- <option value="dd.MM HH:mm">dd.MM HH:mm</option>
83
- <option value="dd.MM">dd.MM</option>
84
- </select>
85
- <label>Farge</label><input id="clk_color" class="small" value="#ffffff">
86
- <label>Oppdater ms</label><input id="clk_ms" class="small" type="number" value="1000" min="200">
87
- </div>
88
- </div>
89
- <div id="typeEntur" style="display:none">
90
- <div class="row">
91
- <label>StopPlace</label><input id="en_stop" class="mid" value="NSR:StopPlace:58287">
92
- <label>Modus</label>
93
- <select id="en_mode"><option value="departures">Fra</option><option value="arrivals">Til</option></select>
94
- </div>
95
- <div class="row">
96
- <label>Limit</label><input id="en_limit" class="small" type="number" value="6" min="1" max="20">
97
- <label>Hent hver s</label><input id="en_fetch" class="small" type="number" value="30" min="5">
98
- <label>Linjer</label><input id="en_lines" class="small" type="number" value="6" min="1" max="20">
99
- </div>
100
- <div class="row">
101
- <label>Vy</label><input id="en_vy" type="checkbox" checked>
102
- <label>SJ</label><input id="en_sj" type="checkbox" checked>
103
- <label>Flytoget</label><input id="en_fly" type="checkbox" checked>
104
- <label>Forsinket</label><input id="en_onlydel" type="checkbox">
105
- <label>Kansellert</label><input id="en_onlycan" type="checkbox">
106
- </div>
107
- <div class="row">
108
- <label>Template per rad</label>
109
- <input id="en_tpl" class="wide mono" value="[{icon}] {time} {line|cut=6} {dest|cutEnd=12} Sp{quay|pad=2} {status} {delay}">
110
- </div>
111
- <div class="row">
112
- <label>Farge normal</label><input id="en_c_norm" class="small" value="#ffffff">
113
- <label>Farge forsinket</label><input id="en_c_del" class="small" value="#ffe66d">
114
- <label>Farge kansellert</label><input id="en_c_can" class="small" value="#ff5c5c">
115
- </div>
116
- <div class="row">
117
- <button id="en_fetch_now">Hent nå</button>
118
- <button id="en_preview">Forhåndsvis</button>
119
- </div>
120
- </div>
121
- <div id="typeText" style="display:none">
122
- <div class="row">
123
- <label>Tekst</label><input id="tx_text" class="wide" value="Velkommen">
124
- </div>
125
- <div class="row">
126
- <label>Farge</label><input id="tx_color" class="small" value="#6daaff">
127
- </div>
128
- </div>
129
- </div>
130
- </div>
131
-
132
- <h3>Scene</h3>
133
- <div class="row">
134
- <label>Auto-push s</label><input id="scene_push" class="small" type="number" value="10" min="3">
135
- <button id="startAuto">Start auto</button>
136
- <button id="stopAuto">Stopp</button>
137
- </div>
138
-
139
- <h3>Lagring</h3>
140
- <div class="row">
141
- <button id="save">Lagre</button>
142
- <button id="load">Last</button>
143
- <button id="export">Eksporter</button>
144
- <input id="importFile" type="file" accept="application/json">
145
- </div>
146
- </div>
147
-
148
- <!-- høyre -->
149
- <div class="panel">
150
- <div class="row">
151
- <strong>Canvas</strong>
152
- <span>Kol × Rad:</span><span id="dim" class="badge">20 × 10</span>
153
- </div>
154
- <div id="canvas" class="gridwrap" style="--cols:20;--rows:10">
155
- <div id="grid" class="grid" style="--cols:20;--rows:10"></div>
156
- </div>
157
- <h3>Logg</h3>
158
- <div id="log" class="list"></div>
159
- </div>
160
- </div>
161
-
162
- <script>
163
- const S=id=>document.getElementById(id);
164
- const statusEl=S('status'); const logEl=S('log');
165
- function setStatus(s){ statusEl.textContent=s; }
166
- function log(m,d){ const t=new Date().toLocaleTimeString(); const line=`[${t}] ${m}${d? " "+JSON.stringify(d):""}`; logEl.textContent=line+"\n"+logEl.textContent; console.log(m,d??""); }
167
- window.addEventListener('error',e=>{log('JS',{m:e.message,src:e.filename,l:e.lineno});});
168
-
169
- const ENTUR_URL = 'https://api.entur.io/journey-planner/v3/graphql';
170
- // ved CORS: const ENTUR_URL = '/cb/proxy_entur.php';
171
-
172
- const state = {
173
- cols: parseInt(S('cols').value,10),
174
- rows: parseInt(S('rows').value,10),
175
- tiles: [],
176
- sel: null,
177
- autoPushTimer: null
178
- };
179
-
180
- function uid(){ return Math.random().toString(36).slice(2,8); }
181
- function snap(v, max){ v=Math.max(0, Math.min(v, max)); return v; }
182
- function updateGrid(){
183
- const g = S('grid'); const wrap = S('canvas');
184
- g.style.setProperty('--cols', state.cols);
185
- g.style.setProperty('--rows', state.rows);
186
- wrap.style.setProperty('--cols', state.cols);
187
- wrap.style.setProperty('--rows', state.rows);
188
- S('dim').textContent = `${state.cols} × ${state.rows}`;
189
- }
190
- S('cols').oninput=()=>{ state.cols=parseInt(S('cols').value,10)||20; updateGrid(); renderTiles(); drawTileContent(); };
191
- S('rows').oninput=()=>{ state.rows=parseInt(S('rows').value,10)||10; updateGrid(); renderTiles(); drawTileContent(); };
192
-
193
- function iconMapFor(mode){
194
- const m=(mode||'').toLowerCase();
195
- if(m==='rail') return S('icoRail').value||'🚆';
196
- if(m==='bus') return S('icoBus').value||'🚌';
197
- if(m==='tram') return S('icoTram').value||'🚊';
198
- if(m==='metro') return S('icoMetro').value||'🚇';
199
- if(m==='water') return S('icoWater').value||'🛥️';
200
- if(m==='air') return S('icoAir').value||'✈️';
201
- return S('icoOther').value||'•';
202
- }
203
-
204
- /* Canvas */
205
- const canvas=S('canvas');
206
- function cellToRect(x,y,w,h){
207
- const r=canvas.getBoundingClientRect();
208
- return {
209
- left: (x/state.cols)*r.width,
210
- top: (y/state.rows)*r.height,
211
- width: (w/state.cols)*r.width,
212
- height: (h/state.rows)*r.height
213
- };
214
- }
215
- function rectToCell(left,top,width,height){
216
- const r=canvas.getBoundingClientRect();
217
- return {
218
- x: Math.round(left / r.width * state.cols),
219
- y: Math.round(top / r.height * state.rows),
220
- w: Math.max(1, Math.round(width / r.width * state.cols)),
221
- h: Math.max(1, Math.round(height/ r.height * state.rows))
222
- };
223
- }
224
-
225
- function renderTiles(){
226
- [...canvas.querySelectorAll('.tile')].forEach(n=>n.remove());
227
- for(const t of state.tiles){
228
- const el=document.createElement('div'); el.className='tile'; el.dataset.id=t.id;
229
- const rect=cellToRect(t.x,t.y,t.w,t.h);
230
- el.style.left=rect.left+'px'; el.style.top=rect.top+'px'; el.style.width=rect.width+'px'; el.style.height=rect.height+'px';
231
- el.textContent=tileLabel(t);
232
- const h=document.createElement('div'); h.className='handle'; el.appendChild(h);
233
- el.onclick=(e)=>{ e.stopPropagation(); selectTile(t.id); };
234
- let sx,sy,sl,st, resizing=false;
235
- el.addEventListener('mousedown', e=>{
236
- if(e.target===h){ resizing=true; }
237
- sx=e.clientX; sy=e.clientY;
238
- const bb=el.getBoundingClientRect(); sl=bb.left; st=bb.top;
239
- document.onmousemove=ev=>{
240
- const dx=ev.clientX-sx, dy=ev.clientY-sy;
241
- if(!resizing){
242
- const r=canvas.getBoundingClientRect();
243
- let nl=sl+dx, nt=st+dy;
244
- nl=Math.max(r.left, Math.min(nl, r.right - el.offsetWidth));
245
- nt=Math.max(r.top, Math.min(nt, r.bottom - el.offsetHeight));
246
- const c=rectToCell(nl-r.left, nt-r.top, el.offsetWidth, el.offsetHeight);
247
- t.x=snap(c.x, state.cols-1); t.y=snap(c.y, state.rows-1);
248
- }else{
249
- const nw=Math.max(20, el.offsetWidth + dx), nh=Math.max(20, el.offsetHeight + dy);
250
- const c=rectToCell(el.offsetLeft, el.offsetTop, nw, nh);
251
- t.w=snap(c.w, state.cols - t.x); t.h=snap(c.h, state.rows - t.y);
252
- sx=ev.clientX; sy=ev.clientY;
253
- }
254
- renderTiles(); updateProps(); drawTileContent();
255
- };
256
- document.onmouseup=()=>{ document.onmousemove=null; document.onmouseup=null; resizing=false; };
257
- });
258
- if(state.sel===t.id) el.classList.add('sel');
259
- canvas.appendChild(el);
260
- }
261
- }
262
- canvas.onclick=()=>{ selectTile(null); };
263
-
264
- function selectTile(id){
265
- state.sel=id;
266
- renderTiles();
267
- updateProps();
268
- drawTileContent();
269
- }
270
-
271
- function tileLabel(t){
272
- if(t.type==='clock') return `🕒 ${t.cfg.format}`;
273
- if(t.type==='entur') return `Entur ${t.cfg.mode} ${t.cfg.lines}r`;
274
- if(t.type==='text') return `Tekst`;
275
- return t.type;
276
- }
277
-
278
- /* Tiles CRUD */
279
- function addTile(type, cfg={}){
280
- const t={ id:uid(), type, x:1, y:1, w:8, h:2, cfg };
281
- state.tiles.push(t);
282
- selectTile(t.id);
283
- }
284
- S('addClock').onclick=()=>addTile('clock',{ format:'HH:mm', color:'#ffffff', tick:1000 });
285
- S('addEntur').onclick=()=>addTile('entur',{
286
- stopId:'NSR:StopPlace:58287', mode:'departures', limit:6, fetchSec:30, lines:6,
287
- tpl:'[{icon}] {time} {line|cut=6} {dest|cutEnd=12} Sp{quay|pad=2} {status} {delay}',
288
- cNorm:'#ffffff', cDel:'#ffe66d', cCan:'#ff5c5c',
289
- filters:{vy:true,sj:true,fly:true,onlyDelayed:false,onlyCancelled:false},
290
- cache:[]
291
- });
292
- S('addText').onclick=()=>addTile('text',{ text:'Velkommen', color:'#6daaff' });
293
- S('dupTile').onclick=()=>{ if(!state.sel) return; const src=state.tiles.find(t=>t.id===state.sel); const c=JSON.parse(JSON.stringify(src)); c.id=uid(); c.x+=1; c.y+=1; state.tiles.push(c); selectTile(c.id); };
294
- S('delTile').onclick=()=>{ if(!state.sel) return; state.tiles=state.tiles.filter(t=>t.id!==state.sel); selectTile(null); };
295
-
296
- /* Egenskaper */
297
- function updateProps(){
298
- const t=state.tiles.find(x=>x.id===state.sel);
299
- S('p_id').value=t? t.id:''; S('p_type').value=t? t.type:'';
300
- S('p_x').value=t? t.x:''; S('p_y').value=t? t.y:''; S('p_w').value=t? t.w:''; S('p_h').value=t? t.h:'';
301
- S('typeClock').style.display = t&&t.type==='clock' ? '' : 'none';
302
- S('typeEntur').style.display = t&&t.type==='entur' ? '' : 'none';
303
- S('typeText').style.display = t&&t.type==='text' ? '' : 'none';
304
- if(!t) return;
305
-
306
- if(t.type==='clock'){
307
- S('clk_fmt').value=t.cfg.format; S('clk_color').value=t.cfg.color; S('clk_ms').value=t.cfg.tick;
308
- }
309
- if(t.type==='entur'){
310
- S('en_stop').value=t.cfg.stopId; S('en_mode').value=t.cfg.mode; S('en_limit').value=t.cfg.limit;
311
- S('en_fetch').value=t.cfg.fetchSec; S('en_lines').value=t.cfg.lines;
312
- S('en_tpl').value=t.cfg.tpl; S('en_c_norm').value=t.cfg.cNorm; S('en_c_del').value=t.cfg.cDel; S('en_c_can').value=t.cfg.cCan;
313
- S('en_vy').checked=t.cfg.filters.vy; S('en_sj').checked=t.cfg.filters.sj; S('en_fly').checked=t.cfg.filters.fly;
314
- S('en_onlydel').checked=t.cfg.filters.onlyDelayed; S('en_onlycan').checked=t.cfg.filters.onlyCancelled;
315
- }
316
- if(t.type==='text'){
317
- S('tx_text').value=t.cfg.text; S('tx_color').value=t.cfg.color;
318
- }
319
- }
320
- ['p_x','p_y','p_w','p_h'].forEach(id=>{
321
- S(id).oninput=()=>{ const t=state.tiles.find(x=>x.id===state.sel); if(!t) return;
322
- t.x=+S('p_x').value; t.y=+S('p_y').value; t.w=+S('p_w').value; t.h=+S('p_h').value;
323
- renderTiles(); drawTileContent();
324
- };
325
- });
326
-
327
- S('clk_fmt').onchange=()=>{ const t=getSel('clock'); if(!t) return; t.cfg.format=S('clk_fmt').value; drawTileContent(); };
328
- S('clk_color').oninput=()=>{ const t=getSel('clock'); if(!t) return; t.cfg.color=S('clk_color').value; drawTileContent(); };
329
- S('clk_ms').oninput=()=>{ const t=getSel('clock'); if(!t) return; t.cfg.tick=parseInt(S('clk_ms').value,10)||1000; };
330
-
331
- S('en_stop').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.stopId=S('en_stop').value.trim(); };
332
- S('en_mode').onchange=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.mode=S('en_mode').value; };
333
- S('en_limit').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.limit=parseInt(S('en_limit').value,10)||6; };
334
- S('en_fetch').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.fetchSec=parseInt(S('en_fetch').value,10)||30; restartEnturTimer(t); };
335
- S('en_lines').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.lines=parseInt(S('en_lines').value,10)||6; drawTileContent(); };
336
- S('en_tpl').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.tpl=S('en_tpl').value; drawTileContent(); };
337
- S('en_c_norm').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.cNorm=S('en_c_norm').value; drawTileContent(); };
338
- S('en_c_del').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.cDel=S('en_c_del').value; drawTileContent(); };
339
- S('en_c_can').oninput=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.cCan=S('en_c_can').value; drawTileContent(); };
340
- S('en_vy').onchange=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.filters.vy=S('en_vy').checked; };
341
- S('en_sj').onchange=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.filters.sj=S('en_sj').checked; };
342
- S('en_fly').onchange=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.filters.fly=S('en_fly').checked; };
343
- S('en_onlydel').onchange=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.filters.onlyDelayed=S('en_onlydel').checked; };
344
- S('en_onlycan').onchange=()=>{ const t=getSel('entur'); if(!t) return; t.cfg.filters.onlyCancelled=S('en_onlycan').checked; };
345
-
346
- function getSel(type){ const t=state.tiles.find(x=>x.id===state.sel); if(!t||t.type!==type) return null; return t; }
347
-
348
- /* Template motor */
349
- function applyFilter(val, f){
350
- if(f==='upper') return String(val).toUpperCase();
351
- if(f==='lower') return String(val).toLowerCase();
352
- let m=f.match(/^pad=(\d+)$/); if(m) return String(val).padStart(+m[1],' ');
353
- m=f.match(/^padEnd=(\d+)$/); if(m) return String(val).padEnd(+m[1],' ');
354
- m=f.match(/^cut=(\d+)$/); if(m){ const n=+m[1]; const s=String(val); return s.length>n? s.slice(0,n):s; }
355
- m=f.match(/^cutEnd=(\d+)$/); if(m){ const n=+m[1]; const s=String(val); return s.length>n? s.slice(0,n-1)+'…':s; }
356
- return val;
357
- }
358
- function renderWithTemplate(tpl, dict){
359
- return tpl.replace(/\{([a-z]+)(?::([A-Za-z0-9:]+))?((?:\|[A-Za-z]+(?:=\d+)?)*)\}/g,(m,k,fmt,filters)=>{
360
- let v=dict[k]??'';
361
- if((k==='time'||k==='aimed') && fmt){ if(fmt==='HHmm') v=String(v).replace(':',''); }
362
- if(filters){ for(const f of filters.split('|').filter(Boolean)) v=applyFilter(v,f); }
363
- return v;
364
- });
365
- }
366
-
367
- /* Entur fetch */
368
- async function enturFetch(stopId, mode){
369
- const q=`query Q($id:String!,$now:DateTime!){
370
- stopPlace(id:$id){
371
- estimatedCalls(startTime:$now,timeRange:14400,numberOfDepartures:120){
372
- aimedDepartureTime expectedDepartureTime
373
- aimedArrivalTime expectedArrivalTime
374
- realtime cancellation
375
- destinationDisplay{frontText}
376
- quay{name}
377
- serviceJourney{ line{name transportMode operator{name}} }
378
- }
379
- }
380
- }`;
381
- const r=await fetch(ENTUR_URL,{method:'POST',headers:{
382
- 'Content-Type':'application/json','Accept':'application/json','ET-Client-Name':'samsai-splitflap-admin'
383
- },cache:'no-store',body:JSON.stringify({query:q,variables:{id:stopId,now:new Date().toISOString()}})});
384
- const txt=await r.text(); if(!r.ok){ log('entur http',{st:r.status, body:txt.slice(0,200)}); throw new Error('entur '+r.status); }
385
- const j=JSON.parse(txt); const calls=j?.data?.stopPlace?.estimatedCalls||[];
386
- return calls.map(c=>{
387
- const aimedISO = mode==='arrivals' ? (c.aimedArrivalTime||c.aimedDepartureTime) : (c.aimedDepartureTime||c.aimedArrivalTime);
388
- const expISO = mode==='arrivals' ? (c.expectedArrivalTime||aimedISO) : (c.expectedDepartureTime||aimedISO);
389
- const aimed=aimedISO?new Date(aimedISO):null; const exp=expISO?new Date(expISO):aimed;
390
- const time=expISO?expISO.slice(11,16):''; const aimedT=aimedISO?aimedISO.slice(11,16):'';
391
- const delay=aimed&&exp? Math.round((exp-aimed)/60000) : 0;
392
- const status=c.cancellation?'CANCELLED':'';
393
- const transport=c?.serviceJourney?.line?.transportMode||'';
394
- const op=c?.serviceJourney?.line?.operator?.name||'';
395
- return {
396
- time, aimed:aimedT, delay, status,
397
- dest:(c?.destinationDisplay?.frontText||'').trim(),
398
- quay:(c?.quay?.name||'').trim(),
399
- line:(c?.serviceJourney?.line?.name||'').trim(),
400
- op: op.trim(), transport: transport.trim(),
401
- ts: exp?exp.getTime():9e15
402
- };
403
- }).sort((a,b)=>a.ts-b.ts);
404
- }
405
- function enturFilter(items, f){
406
- return items.filter(it=>{
407
- if(f.onlyDelayed && !(it.delay>0)) return false;
408
- if(f.onlyCancelled && it.status!=='CANCELLED') return false;
409
- const ops=[]; if(f.vy) ops.push('vy'); if(f.sj) ops.push('sj'); if(f.fly) ops.push('flytoget');
410
- if(ops.length){ const o=(it.op||'').toLowerCase(); if(!ops.some(x=>o.includes(x))) return false; }
411
- return true;
412
- });
413
- }
414
- function enturRenderLines(t, items){
415
- const lines = [];
416
- const max = t.cfg.lines;
417
- for(const it of items.slice(0, max)){
418
- const dict={...it, icon:iconMapFor(it.transport), mode:t.cfg.mode==='arrivals'?'ANK':'FRA'};
419
- const text=renderWithTemplate(t.cfg.tpl, dict);
420
- const color = it.status==='CANCELLED' ? t.cfg.cCan : (it.delay>0 ? t.cfg.cDel : t.cfg.cNorm);
421
- lines.push({text,color});
422
- }
423
- if(lines.length===0){ lines.push({text:'Ingen data', color:t.cfg.cNorm}); }
424
- return lines;
425
- }
426
-
427
- /* Tile timers */
428
- const enturTimers = new Map();
429
- function restartEnturTimer(t){
430
- if(enturTimers.has(t.id)){ clearInterval(enturTimers.get(t.id)); enturTimers.delete(t.id); }
431
- const ms=Math.max(5000, (t.cfg.fetchSec||30)*1000);
432
- const fn=async()=>{
433
- try{
434
- const raw=await enturFetch(t.cfg.stopId, t.cfg.mode);
435
- const filtered=enturFilter(raw, t.cfg.filters);
436
- t.cfg.cache = filtered.length? filtered : raw;
437
- log('entur ok',{id:t.id, rows:t.cfg.cache.length});
438
- drawTileContent();
439
- }catch(e){ log('entur fail',{id:t.id,err:String(e)}); }
440
- };
441
- fn();
442
- const h=setInterval(fn, ms);
443
- enturTimers.set(t.id, h);
444
- }
445
-
446
- /* Preview rendering som matcher tavla */
447
- function cutToWidth(text, w){
448
- if(!w) return text;
449
- const s=String(text);
450
- return s.length<=w ? s : s.slice(0, w);
451
- }
452
- function drawTileContent(){
453
- for(const el of canvas.querySelectorAll('.tile')){
454
- const t=state.tiles.find(x=>x.id===el.dataset.id);
455
- if(!t) continue;
456
- if(t.type==='clock'){
457
- el.textContent = nowFormat(t.cfg.format);
458
- el.style.color = t.cfg.color;
459
- }else if(t.type==='entur'){
460
- const lines = enturRenderLines(t, t.cfg.cache||[]);
461
- const maxLines = Math.max(1, t.h);
462
- el.innerHTML = lines.slice(0,maxLines)
463
- .map(l=>`<div style="color:${l.color}">${escapeHtml(cutToWidth(l.text, t.w))}</div>`).join('');
464
- }else if(t.type==='text'){
465
- el.innerHTML = `<div style="color:${t.cfg.color}">${escapeHtml(cutToWidth(t.cfg.text, t.w))}</div>`;
466
- }
467
- }
468
- }
469
- function nowFormat(fmt){
470
- const d=new Date();
471
- const pad=n=>String(n).padStart(2,'0');
472
- if(fmt==='HH:mm') return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
473
- if(fmt==='HH:mm:ss') return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
474
- if(fmt==='dd.MM HH:mm') return `${pad(d.getDate())}.${pad(d.getMonth()+1)} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
475
- if(fmt==='dd.MM') return `${pad(d.getDate())}.${pad(d.getMonth()+1)}`;
476
- return d.toLocaleTimeString();
477
- }
478
- function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
479
- let clockTimer=setInterval(()=>{ drawTileContent(); }, 1000);
480
-
481
- /* Push til display, per tile-posisjon */
482
- function buildCommandsFromTiles(){
483
- const cmds = [];
484
- const sorted = [...state.tiles].sort((a,b)=> a.y-b.y || a.x-b.x);
485
- for(const t of sorted){
486
- if(t.type==='clock'){
487
- const text = cutToWidth(nowFormat(t.cfg.format), t.w);
488
- cmds.push({ cmd:'display', args:{ text, color:t.cfg.color, x:t.x, y:t.y, sound:false } });
489
- }else if(t.type==='text'){
490
- const text = cutToWidth(t.cfg.text, t.w);
491
- cmds.push({ cmd:'display', args:{ text, color:t.cfg.color, x:t.x, y:t.y, sound:false } });
492
- }else if(t.type==='entur'){
493
- const lines = enturRenderLines(t, t.cfg.cache||[]);
494
- const maxLines = Math.max(1, t.h);
495
- lines.slice(0, maxLines).forEach((l, i)=>{
496
- const text = cutToWidth(l.text, t.w);
497
- cmds.push({ cmd:'display', args:{ text, color:l.color, x:t.x, y:t.y+i, sound:false } });
498
- });
499
- }
500
- }
501
- return cmds;
502
- }
503
- async function pushCommands(cmds){
504
- const API=S('apiUrl').value.trim(), ROOM=S('room').value.trim(), TOKEN=S('token').value.trim();
505
- await fetch(`${API}?action=push&room=${encodeURIComponent(ROOM)}`,{
506
- method:'POST', headers:{'Content-Type':'application/json'}, cache:'no-store',
507
- body:JSON.stringify({token:TOKEN,cmd:'clear',args:{}})
508
- });
509
- for(const m of cmds){
510
- await fetch(`${API}?action=push&room=${encodeURIComponent(ROOM)}`,{
511
- method:'POST', headers:{'Content-Type':'application/json'}, cache:'no-store',
512
- body:JSON.stringify({token:TOKEN,cmd:m.cmd,args:m.args})
513
- });
514
- }
515
- }
516
-
517
- /* Kontroller */
518
- S('ping').onclick=async()=>{
519
- const url=`${S('apiUrl').value.trim()}?action=fetch&room=${encodeURIComponent(S('room').value.trim())}&since=0`;
520
- const r=await fetch(url,{cache:'no-store'}); const t=await r.text();
521
- log('ping',{st:r.status, body:t.slice(0,160)});
522
- };
523
- S('clear').onclick=async()=>{
524
- const API=S('apiUrl').value.trim(), ROOM=S('room').value.trim(), TOKEN=S('token').value.trim();
525
- const r=await fetch(`${API}?action=push&room=${encodeURIComponent(ROOM)}`,{method:'POST',headers:{'Content-Type':'application/json'},cache:'no-store',body:JSON.stringify({token:TOKEN,cmd:'clear',args:{}})});
526
- log('clear', {st:r.status});
527
- };
528
- S('pushScene').onclick=async()=>{
529
- const cmds = buildCommandsFromTiles();
530
- await pushCommands(cmds);
531
- log('push scene', {rows:cmds.length});
532
- };
533
-
534
- /* Auto-push scene */
535
- S('startAuto').onclick=()=>{
536
- const s=Math.max(3, parseInt(S('scene_push').value,10)||10)*1000;
537
- if(state.autoPushTimer) clearInterval(state.autoPushTimer);
538
- state.autoPushTimer=setInterval(async()=>{
539
- drawTileContent();
540
- const cmds = buildCommandsFromTiles();
541
- try{ await pushCommands(cmds); }catch{}
542
- }, s);
543
- for(const t of state.tiles){ if(t.type==='entur') restartEnturTimer(t); }
544
- log('auto start',{sec:s/1000});
545
- };
546
- S('stopAuto').onclick=()=>{
547
- if(state.autoPushTimer) clearInterval(state.autoPushTimer);
548
- state.autoPushTimer=null;
549
- for(const h of enturTimers.values()) clearInterval(h);
550
- enturTimers.clear();
551
- log('auto stop');
552
- };
553
-
554
- /* Entur knapper i props */
555
- S('en_fetch_now').onclick=async()=>{
556
- const t=getSel('entur'); if(!t) return;
557
- try{
558
- const raw=await enturFetch(t.cfg.stopId, t.cfg.mode);
559
- t.cfg.cache=enturFilter(raw, t.cfg.filters);
560
- drawTileContent();
561
- log('entur now',{rows:t.cfg.cache.length});
562
- }catch(e){ log('entur now fail', String(e)); }
563
- };
564
- S('en_preview').onclick=()=>{ drawTileContent(); };
565
-
566
- /* Lagring */
567
- S('save').onclick=()=>{ const data={cols:state.cols,rows:state.rows,tiles:state.tiles,icons:{
568
- rail:S('icoRail').value,bus:S('icoBus').value,tram:S('icoTram').value,metro:S('icoMetro').value,water:S('icoWater').value,air:S('icoAir').value,other:S('icoOther').value
569
- }}; localStorage.setItem('cb_canvas', JSON.stringify(data)); log('lagret'); };
570
- S('load').onclick=()=>{ const raw=localStorage.getItem('cb_canvas'); if(!raw) return; try{
571
- const d=JSON.parse(raw); state.cols=d.cols||20; state.rows=d.rows||10; state.tiles=d.tiles||[];
572
- S('cols').value=state.cols; S('rows').value=state.rows;
573
- const ic=d.icons||{}; S('icoRail').value=ic.rail||'🚆'; S('icoBus').value=ic.bus||'🚌'; S('icoTram').value=ic.tram||'🚊'; S('icoMetro').value=ic.metro||'🚇'; S('icoWater').value=ic.water||'🛥️'; S('icoAir').value=ic.air||'✈️'; S('icoOther').value=ic.other||'•';
574
- updateGrid(); renderTiles(); drawTileContent(); log('lastet',{tiles:state.tiles.length});
575
- }catch(e){ log('load fail', String(e)); } };
576
- S('export').onclick=()=>{ const blob=new Blob([localStorage.getItem('cb_canvas')||'{}'],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='cb_canvas.json'; a.click(); };
577
- S('importFile').onchange=async(e)=>{ const f=e.target.files[0]; if(!f) return; const txt=await f.text(); localStorage.setItem('cb_canvas', txt); S('load').click(); };
578
-
579
- /* Init */
580
- updateGrid(); renderTiles(); drawTileContent(); setStatus('ok');
581
- </script>
582
- </body>
583
- </html>