BruceBanners commited on
Commit
5273c56
Β·
verified Β·
1 Parent(s): 763b235

Make the design and functinality of my site better

Browse files
Files changed (2) hide show
  1. README.md +9 -5
  2. index.html +582 -18
README.md CHANGED
@@ -1,10 +1,14 @@
1
  ---
2
- title: Undefined
3
- emoji: πŸ“Š
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: undefined
3
+ colorFrom: green
4
+ colorTo: green
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://deepsite.hf.co).
14
+
index.html CHANGED
@@ -1,19 +1,583 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
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>