iobroker.f1 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,450 @@
1
+ /*
2
+ ioBroker.f1 — Session Schedule Widget
3
+ VIS1 Widget Implementation
4
+ */
5
+
6
+ /* global $, vis */
7
+
8
+ (function () {
9
+ "use strict";
10
+
11
+ // ── Lookup tables ───────────────────────────────────────────────────────
12
+ var FLAGS = {
13
+ JPN: "🇯🇵", AUS: "🇦🇺", BHR: "🇧🇭", SAU: "🇸🇦", USA: "🇺🇸",
14
+ ITA: "🇮🇹", MCO: "🇲🇨", ESP: "🇪🇸", CAN: "🇨🇦", AUT: "🇦🇹",
15
+ GBR: "🇬🇧", HUN: "🇭🇺", BEL: "🇧🇪", NLD: "🇳🇱", SGP: "🇸🇬",
16
+ AZE: "🇦🇿", MEX: "🇲🇽", BRA: "🇧🇷", QAT: "🇶🇦", ARE: "🇦🇪",
17
+ CHN: "🇨🇳", POR: "🇵🇹"
18
+ };
19
+
20
+ var GP_NAMES = {
21
+ "Japan": "Japanese GP", "Australia": "Australian GP",
22
+ "Bahrain": "Bahrain GP", "Saudi Arabia": "Saudi Arabian GP",
23
+ "United States": "United States GP", "Italy": "Italian GP",
24
+ "Monaco": "Monaco GP", "Spain": "Spanish GP",
25
+ "Canada": "Canadian GP", "Austria": "Austrian GP",
26
+ "Great Britain": "British GP", "Hungary": "Hungarian GP",
27
+ "Belgium": "Belgian GP", "Netherlands": "Dutch GP",
28
+ "Singapore": "Singapore GP", "Azerbaijan": "Azerbaijan GP",
29
+ "Mexico": "Mexican GP", "Brazil": "Brazilian GP",
30
+ "Qatar": "Qatar GP", "Abu Dhabi": "Abu Dhabi GP",
31
+ "China": "Chinese GP", "Portugal": "Portuguese GP"
32
+ };
33
+
34
+ var I18N = {
35
+ schedule: { de: "Session-Zeitplan", en: "Session Schedule" },
36
+ timeIn: { de: "Zeiten in", en: "Times in" },
37
+ sessions: { de: "Sessions", en: "Sessions" },
38
+ circuit: { de: "Strecke", en: "Circuit" },
39
+ date: { de: "Datum", en: "Date" },
40
+ live: { de: "Live", en: "Live" },
41
+ next: { de: "Nächste", en: "Next" },
42
+ done: { de: "Beendet", en: "Done" },
43
+ planned: { de: "Geplant", en: "Planned" },
44
+ upcoming: { de: "KOMMEND", en: "UPCOMING" },
45
+ noOid: { de: "Kein OID konfiguriert", en: "No OID configured" },
46
+ waiting: { de: "Warte auf F1-Daten…", en: "Waiting for F1 data…" },
47
+ invalid: { de: "Ungültige Daten", en: "Invalid data" }
48
+ };
49
+
50
+ // ── Pure helpers ────────────────────────────────────────────────────────
51
+ function pad2(n) {
52
+ return (n < 10 ? "0" : "") + n;
53
+ }
54
+
55
+ function i18n(key, lang) {
56
+ var row = I18N[key];
57
+ if (!row) return key;
58
+ return row[lang] || row["en"];
59
+ }
60
+
61
+ function langToLocale(lang) {
62
+ return lang === "en" ? "en-GB" : "de-AT";
63
+ }
64
+
65
+ function fmtTime(dateStr, tz, lang) {
66
+ return new Date(dateStr).toLocaleTimeString(langToLocale(lang), {
67
+ hour: "2-digit", minute: "2-digit", timeZone: tz
68
+ });
69
+ }
70
+
71
+ function fmtDateShort(dateStr, tz, lang) {
72
+ return new Date(dateStr).toLocaleDateString(langToLocale(lang), {
73
+ day: "2-digit", month: "2-digit", timeZone: tz
74
+ });
75
+ }
76
+
77
+ function fmtDateLong(dateStr, tz, lang) {
78
+ return new Date(dateStr).toLocaleDateString(langToLocale(lang), {
79
+ weekday: "short", day: "2-digit", month: "short", timeZone: tz
80
+ });
81
+ }
82
+
83
+ function sessionStatus(s) {
84
+ var now = Date.now();
85
+ var start = new Date(s.date_start).getTime();
86
+ var end = new Date(s.date_end).getTime();
87
+ if (now >= start && now <= end) return "live";
88
+ if (now < start) return "upcoming";
89
+ return "completed"; // also covers missing/invalid dates (NaN comparisons → false)
90
+ }
91
+
92
+ function sessionIcon(s) {
93
+ if (s.session_type === "Practice") {
94
+ var m = s.session_name.match(/(\d)/);
95
+ return "FP" + (m ? m[1] : "");
96
+ }
97
+ if (s.session_type === "Qualifying") return "Q";
98
+ if (s.session_type === "Race") return "R";
99
+ // Check compound sprint names before the generic Sprint type
100
+ if (s.session_name.indexOf("Sprint Qualifying") >= 0) return "SQ";
101
+ if (s.session_name.indexOf("Sprint Shootout") >= 0) return "SS";
102
+ if (s.session_type === "Sprint" || s.session_type.indexOf("Sprint") >= 0) return "SPR";
103
+ return s.session_type.substring(0, 2).toUpperCase();
104
+ }
105
+
106
+ function iconColors(s) {
107
+ var t = s.session_type.toLowerCase();
108
+ if (t.indexOf("practice") >= 0) return { bg: "rgba(0,188,212,0.1)", color: "#00bcd4", border: "rgba(0,188,212,0.25)" };
109
+ if (t.indexOf("qualifying") >= 0) return { bg: "rgba(255,214,0,0.1)", color: "#ffd600", border: "rgba(255,214,0,0.25)" };
110
+ if (t === "race") return { bg: "rgba(225,6,0,0.12)", color: "#e10600", border: "rgba(225,6,0,0.3)" };
111
+ if (t.indexOf("sprint") >= 0) return { bg: "rgba(255,152,0,0.1)", color: "#ff9800", border: "rgba(255,152,0,0.25)" };
112
+ return { bg: "rgba(0,188,212,0.1)", color: "#00bcd4", border: "rgba(0,188,212,0.25)" };
113
+ }
114
+
115
+ // ── Widget namespace ────────────────────────────────────────────────────
116
+ vis.binds["f1"] = vis.binds["f1"] || {};
117
+
118
+ // Test-only export of pure helpers (stripped/ignored in production)
119
+ vis.binds["f1"]._test = {
120
+ FLAGS: FLAGS, GP_NAMES: GP_NAMES,
121
+ pad2: pad2, i18n: i18n,
122
+ langToLocale: langToLocale,
123
+ fmtTime: fmtTime, fmtDateShort: fmtDateShort, fmtDateLong: fmtDateLong,
124
+ sessionStatus: sessionStatus, sessionIcon: sessionIcon, iconColors: iconColors
125
+ };
126
+
127
+ // ── Placeholder helper ──────────────────────────────────────────────────
128
+ function placeholder(msg, bg) {
129
+ return '<div style="font-family:Segoe UI,sans-serif;background:' + bg +
130
+ ';padding:40px;text-align:center;color:#666;border-radius:8px;width:100%;height:100%;">' +
131
+ msg + '</div>';
132
+ }
133
+
134
+ // ── renderSessions ──────────────────────────────────────────────────────
135
+ vis.binds["f1"].renderSessions = function (sessions, props, widgetID) {
136
+ var lang = props.language || "de";
137
+ var tz = props.timezone || "Europe/Vienna";
138
+ var C = {
139
+ bg: props.color_bg || "#15151f",
140
+ card: props.color_card || "#1a1a28",
141
+ card2: "#1c1c2a",
142
+ border: "#2a2a3a",
143
+ text: props.color_text || "#eeeeef",
144
+ muted: props.color_text_muted || "#666678",
145
+ dim: "#444455",
146
+ accent: props.color_accent || "#e10600",
147
+ green: "#00e676",
148
+ yellow: "#ffd600"
149
+ };
150
+ var hdrSz = props.font_size_header || "22px";
151
+ var sesSz = props.font_size_session || "13px";
152
+ var timSz = props.font_size_time || "16px";
153
+ var cdSz = props.font_size_countdown || "24px";
154
+ var sesPx = parseInt(sesSz, 10) || 13;
155
+ var dateSz = Math.round(sesPx * 0.77) + "px";
156
+ var badgeSz = Math.round(sesPx * 0.85) + "px";
157
+
158
+ if (!sessions || !sessions.length) {
159
+ return placeholder(i18n("waiting", lang), C.bg);
160
+ }
161
+
162
+ var first = sessions[0];
163
+ var flag = FLAGS[first.country_code] || "🏁";
164
+ var gpName = GP_NAMES[first.country_name] || (first.country_name + " GP");
165
+ var dateRange = fmtDateShort(first.date_start, tz, lang) + " – " +
166
+ fmtDateShort(sessions[sessions.length - 1].date_start, tz, lang);
167
+
168
+ var liveSession = null;
169
+ var nextSession = null;
170
+ for (var i = 0; i < sessions.length; i++) {
171
+ var st = sessionStatus(sessions[i]);
172
+ if (st === "live" && !liveSession) liveSession = sessions[i];
173
+ if (st === "upcoming" && !nextSession) nextSession = sessions[i];
174
+ }
175
+
176
+ var html = "";
177
+
178
+ // Wrapper
179
+ html += '<div class="f1-sessions-widget" style="background:' + C.bg + ';color:' + C.text + ';border:1px solid ' + C.border + ';border-radius:8px;">';
180
+
181
+ // Header
182
+ html += '<div style="background:linear-gradient(135deg,' + C.accent + ',#a00400);padding:14px 18px;display:flex;justify-content:space-between;align-items:flex-start;">';
183
+ html += '<div>';
184
+ html += '<div style="font-size:10px;letter-spacing:2px;text-transform:uppercase;color:rgba(255,255,255,0.75);font-weight:600;">' + flag + ' ' + first.country_code + '</div>';
185
+ html += '<div style="font-size:' + hdrSz + ';font-weight:900;text-transform:uppercase;letter-spacing:1px;color:#fff;line-height:1.15;margin-top:1px;">' + gpName + '</div>';
186
+ var circuitLine = first.circuit_short_name === first.location
187
+ ? first.location
188
+ : first.circuit_short_name + ' · ' + first.location;
189
+ html += '<div style="font-size:11px;color:rgba(255,255,255,0.7);margin-top:2px;">' + circuitLine + '</div>';
190
+ html += '</div>';
191
+ html += '<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;">';
192
+ if (liveSession) {
193
+ html += '<span style="display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:3px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;background:' + C.green + ';color:#111;">● LIVE</span>';
194
+ }
195
+ html += '<span style="font-size:14px;font-weight:700;color:rgba(255,255,255,0.6);">' + first.year + '</span>';
196
+ html += '</div></div>';
197
+
198
+ // Info bar
199
+ html += '<div style="display:flex;justify-content:space-around;padding:10px 14px;background:' + C.card2 + ';border-bottom:1px solid ' + C.border + ';">';
200
+ var infoItems = [
201
+ { lbl: i18n("circuit", lang), val: first.circuit_short_name },
202
+ { lbl: i18n("date", lang), val: dateRange },
203
+ { lbl: i18n("sessions", lang), val: String(sessions.length) },
204
+ { lbl: i18n("timeIn", lang), val: tz.split("/")[1] || tz }
205
+ ];
206
+ for (var ii = 0; ii < infoItems.length; ii++) {
207
+ html += '<div style="display:flex;flex-direction:column;align-items:center;gap:2px;">';
208
+ html += '<span style="color:' + C.muted + ';text-transform:uppercase;font-size:8px;letter-spacing:1.2px;font-weight:600;">' + infoItems[ii].lbl + '</span>';
209
+ html += '<span style="color:#ddd;font-weight:700;font-size:11px;">' + infoItems[ii].val + '</span>';
210
+ html += '</div>';
211
+ }
212
+ html += '</div>';
213
+
214
+ // Section title
215
+ html += '<div style="padding:12px 18px 8px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:2.5px;color:#777;display:flex;align-items:center;gap:8px;">';
216
+ html += i18n("schedule", lang);
217
+ html += '<span style="flex:1;height:1px;background:' + C.border + ';display:inline-block;"></span>';
218
+ html += '</div>';
219
+
220
+ // Session rows
221
+ for (var si = 0; si < sessions.length; si++) {
222
+ var s = sessions[si];
223
+ var status = sessionStatus(s);
224
+ var icon = sessionIcon(s);
225
+ var ic = iconColors(s);
226
+ var isLive = status === "live";
227
+ var isNext = !liveSession && nextSession && s.session_key === nextSession.session_key;
228
+
229
+ var rowBg = (si % 2 === 0) ? C.card : C.bg;
230
+ var leftBorder = "none";
231
+ var padLeft = "18px";
232
+ if (isLive) {
233
+ rowBg = "rgba(0,230,118,0.05)"; leftBorder = "3px solid " + C.green; padLeft = "15px";
234
+ } else if (isNext) {
235
+ rowBg = "rgba(255,214,0,0.03)"; leftBorder = "3px solid " + C.yellow; padLeft = "15px";
236
+ }
237
+
238
+ html += '<div style="display:flex;align-items:center;padding:10px 18px 10px ' + padLeft + ';gap:12px;border-bottom:1px solid rgba(42,42,58,0.4);background:' + rowBg + ';border-left:' + leftBorder + ';">';
239
+
240
+ // Badge icon
241
+ html += '<div style="width:38px;height:38px;min-width:38px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:' + badgeSz + ';font-weight:800;background:' + ic.bg + ';color:' + ic.color + ';border:1px solid ' + ic.border + ';">' + icon + '</div>';
242
+
243
+ // Name + date
244
+ html += '<div style="flex:1;min-width:0;">';
245
+ html += '<div style="font-size:' + sesSz + ';font-weight:700;color:' + (status === "completed" ? "#666" : C.text) + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + s.session_name + '</div>';
246
+ html += '<div style="font-size:' + dateSz + ';color:#888;">' + fmtDateLong(s.date_start, tz, lang) + ' · ' + fmtTime(s.date_start, tz, lang) + ' – ' + fmtTime(s.date_end, tz, lang) + '</div>';
247
+ html += '</div>';
248
+
249
+ // Time + tag
250
+ html += '<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;white-space:nowrap;">';
251
+ html += '<span style="font-size:' + timSz + ';font-weight:700;color:' + (status === "completed" ? "#555" : C.text) + ';">' + fmtTime(s.date_start, tz, lang) + '</span>';
252
+ if (isLive) {
253
+ html += '<span style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;padding:2px 7px;border-radius:2px;background:' + C.green + ';color:#111;">' + i18n("live", lang) + '</span>';
254
+ } else if (isNext) {
255
+ html += '<span style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;padding:2px 7px;border-radius:2px;color:' + C.yellow + ';background:rgba(255,214,0,0.12);border:1px solid rgba(255,214,0,0.25);">' + i18n("next", lang) + '</span>';
256
+ } else if (status === "completed") {
257
+ html += '<span style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;padding:2px 7px;border-radius:2px;color:#555;background:rgba(100,100,120,0.1);">' + i18n("done", lang) + '</span>';
258
+ } else {
259
+ html += '<span style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;padding:2px 7px;border-radius:2px;color:#aaa;background:rgba(150,150,180,0.08);">' + i18n("planned", lang) + '</span>';
260
+ }
261
+ html += '</div></div>';
262
+ }
263
+
264
+ // Countdown section
265
+ if (props.show_countdown !== false && (liveSession || nextSession)) {
266
+ var cdLabel = liveSession
267
+ ? (liveSession.session_name + (lang === "de" ? " läuft" : " running"))
268
+ : ((lang === "de" ? "Nächste: " : "Next: ") + nextSession.session_name);
269
+
270
+ html += '<div style="padding:10px 18px 0;font-size:9px;text-transform:uppercase;letter-spacing:2px;color:' + C.muted + ';text-align:center;font-weight:600;background:' + C.card2 + ';border-top:1px solid ' + C.border + ';">' + cdLabel + '</div>';
271
+ html += '<div style="display:flex;justify-content:center;align-items:flex-start;gap:6px;padding:10px 18px 14px;background:' + C.card2 + ';">';
272
+
273
+ var valColor = liveSession ? C.green : C.text;
274
+ var lblStyle = "font-size:7px;text-transform:uppercase;letter-spacing:1.5px;color:" + C.dim + ";font-weight:600;margin-top:3px;";
275
+ var sepStyle = "font-size:" + cdSz + ";font-weight:300;color:" + C.dim + ";line-height:1;";
276
+
277
+ html += '<div style="display:flex;flex-direction:column;align-items:center;min-width:40px;">';
278
+ html += '<span id="' + widgetID + '-cd-d" style="font-size:' + cdSz + ';font-weight:900;line-height:1;color:' + valColor + ';">--</span>';
279
+ html += '<span style="' + lblStyle + '">' + (lang === "de" ? "Tage" : "d") + '</span></div>';
280
+ html += '<span style="' + sepStyle + '">:</span>';
281
+ html += '<div style="display:flex;flex-direction:column;align-items:center;min-width:40px;">';
282
+ html += '<span id="' + widgetID + '-cd-h" style="font-size:' + cdSz + ';font-weight:900;line-height:1;color:' + valColor + ';">--</span>';
283
+ html += '<span style="' + lblStyle + '">' + (lang === "de" ? "Std" : "h") + '</span></div>';
284
+ html += '<span style="' + sepStyle + '">:</span>';
285
+ html += '<div style="display:flex;flex-direction:column;align-items:center;min-width:40px;">';
286
+ html += '<span id="' + widgetID + '-cd-m" style="font-size:' + cdSz + ';font-weight:900;line-height:1;color:' + valColor + ';">--</span>';
287
+ html += '<span style="' + lblStyle + '">' + (lang === "de" ? "Min" : "m") + '</span></div>';
288
+ html += '<span style="' + sepStyle + '">:</span>';
289
+ html += '<div style="display:flex;flex-direction:column;align-items:center;min-width:40px;">';
290
+ html += '<span id="' + widgetID + '-cd-s" style="font-size:' + cdSz + ';font-weight:900;line-height:1;color:' + valColor + ';">--</span>';
291
+ html += '<span style="' + lblStyle + '">' + (lang === "de" ? "Sek" : "s") + '</span></div>';
292
+ html += '</div>';
293
+ }
294
+
295
+ html += '</div>'; // close wrapper
296
+ return html;
297
+ };
298
+
299
+ // ── startCountdown ──────────────────────────────────────────────────────
300
+ vis.binds["f1"].startCountdown = function (widgetID, sessions, props) {
301
+ if (props.show_countdown === false) return;
302
+
303
+ // Cancel any existing countdown interval for this widget instance
304
+ var $widget = $("#" + widgetID);
305
+ var existing = $widget.data("f1-cd-interval");
306
+ if (existing) {
307
+ clearInterval(existing);
308
+ }
309
+
310
+ var liveSession = null;
311
+ var nextSession = null;
312
+ for (var i = 0; i < sessions.length; i++) {
313
+ var st = sessionStatus(sessions[i]);
314
+ if (st === "live" && !liveSession) liveSession = sessions[i];
315
+ if (st === "upcoming" && !nextSession) nextSession = sessions[i];
316
+ }
317
+
318
+ var target = liveSession || nextSession;
319
+ if (!target) return;
320
+
321
+ var targetTime = liveSession
322
+ ? new Date(liveSession.date_end).getTime()
323
+ : new Date(nextSession.date_start).getTime();
324
+
325
+ var interval = setInterval(function () {
326
+ if (!$("#" + widgetID).length) {
327
+ clearInterval(interval);
328
+ return;
329
+ }
330
+
331
+ var diff = targetTime - Date.now();
332
+ if (diff <= 0) {
333
+ clearInterval(interval);
334
+ $("#" + widgetID + "-cd-d").text("00");
335
+ $("#" + widgetID + "-cd-h").text("00");
336
+ $("#" + widgetID + "-cd-m").text("00");
337
+ $("#" + widgetID + "-cd-s").text("00");
338
+ return;
339
+ }
340
+
341
+ var d = Math.floor(diff / 86400000);
342
+ var h = Math.floor((diff % 86400000) / 3600000);
343
+ var m = Math.floor((diff % 3600000) / 60000);
344
+ var s = Math.floor((diff % 60000) / 1000);
345
+
346
+ $("#" + widgetID + "-cd-d").text(pad2(d));
347
+ $("#" + widgetID + "-cd-h").text(pad2(h));
348
+ $("#" + widgetID + "-cd-m").text(pad2(m));
349
+ $("#" + widgetID + "-cd-s").text(pad2(s));
350
+ }, 1000);
351
+
352
+ // Store handle so it can be cancelled on next data update
353
+ $widget.data("f1-cd-interval", interval);
354
+ };
355
+
356
+ // ── createSessionsWidget ────────────────────────────────────────────────
357
+ vis.binds["f1"].createSessionsWidget = function (widgetID, view, data, style) {
358
+ // Defer until VIS has inserted the widget div into the DOM
359
+ setTimeout(function () {
360
+ var $div = $("#" + widgetID);
361
+ if (!$div.length) { return; }
362
+
363
+ var props = {
364
+ oid: "f1.0.weekend_sessions.sessions_json",
365
+ color_accent: data.attr("color_accent") || "#e10600",
366
+ color_bg: data.attr("color_bg") || "#15151f",
367
+ color_card: data.attr("color_card") || "#1a1a28",
368
+ color_text: data.attr("color_text") || "#eeeeef",
369
+ color_text_muted: data.attr("color_text_muted") || "#666678",
370
+ font_size_header: data.attr("font_size_header") || "22px",
371
+ font_size_session:data.attr("font_size_session")|| "13px",
372
+ font_size_time: data.attr("font_size_time") || "16px",
373
+ font_size_countdown: data.attr("font_size_countdown") || "24px",
374
+ timezone: data.attr("timezone") || "Europe/Vienna",
375
+ language: data.attr("language") || "de",
376
+ show_countdown: String(data.attr("show_countdown")) !== "false"
377
+ };
378
+ var oid = props.oid;
379
+ var lang = props.language;
380
+
381
+ if (!oid) {
382
+ $div.html(placeholder(i18n("noOid", lang), props.color_bg));
383
+ return;
384
+ }
385
+
386
+ function applyScale() {
387
+ var designW = 440;
388
+ var $content = $div.find(".f1-sessions-widget");
389
+ if (!$content.length) { return; }
390
+ // Reset to measure natural height of current content
391
+ $content.css({ "transform": "none", "width": designW + "px", "height": "auto" });
392
+ var designH = $content.outerHeight() || 600;
393
+ var w = $div.width() || designW;
394
+ var h = $div.height() || designH;
395
+ var scale = Math.min(w / designW, h / designH);
396
+ $content.css({
397
+ "transform": "scale(" + scale + ")",
398
+ "transform-origin": "0 0",
399
+ "width": designW + "px",
400
+ "height": designH + "px"
401
+ });
402
+ }
403
+
404
+ function applyData(jsonVal) {
405
+ if (!jsonVal) {
406
+ $div.html(placeholder(i18n("waiting", lang), props.color_bg));
407
+ applyScale();
408
+ return;
409
+ }
410
+ var sessions;
411
+ try {
412
+ sessions = typeof jsonVal === "object" ? jsonVal : JSON.parse(jsonVal);
413
+ } catch (e) {
414
+ $div.html(placeholder(i18n("invalid", lang), props.color_bg));
415
+ console.warn("f1-sessions: JSON parse error", e);
416
+ return;
417
+ }
418
+ $div.html(vis.binds["f1"].renderSessions(sessions, props, widgetID));
419
+ applyScale();
420
+ vis.binds["f1"].startCountdown(widgetID, sessions, props);
421
+ }
422
+
423
+ // Re-scale when widget frame is resized in VIS editor
424
+ if (window.ResizeObserver) {
425
+ var ro = new ResizeObserver(function () { applyScale(); });
426
+ ro.observe($div[0]);
427
+ }
428
+
429
+ // Initial state value
430
+ vis.conn.getStates(oid, function (err, states) {
431
+ if (!err && states && states[oid] && states[oid].val) {
432
+ applyData(states[oid].val);
433
+ } else {
434
+ $div.html(placeholder(i18n("waiting", lang), props.color_bg));
435
+ applyScale();
436
+ }
437
+ });
438
+
439
+ // Live updates
440
+ vis.conn.subscribe([oid]);
441
+ vis.conn._socket.on("stateChange", function (id, state) {
442
+ if (id === oid && $("#" + widgetID).length) {
443
+ applyData(state ? state.val : null);
444
+ }
445
+ });
446
+
447
+ }, 0); // end setTimeout
448
+ };
449
+
450
+ }());
@@ -0,0 +1,23 @@
1
+ <!--
2
+ ioBroker.f1 — VIS1 Widgets
3
+ Entry point: loaded by VIS1 from iobroker.vis/www/widgets/f1.html
4
+ -->
5
+
6
+ <!-- ── F1 Sessions Widget ─────────────────────────────────── -->
7
+ <script type="text/javascript" src="widgets/f1-sessions/js/f1-sessions.js"></script>
8
+ <link rel="stylesheet" href="widgets/f1-sessions/css/f1-sessions.css">
9
+
10
+ <script id="tplF1Sessions"
11
+ type="text/ejs"
12
+ class="vis-tpl"
13
+ data-vis-set="f1"
14
+ data-vis-type="static"
15
+ data-vis-name="F1 Sessions"
16
+ data-vis-prev='<div style="width:120px;height:80px;background:#15151f;border-radius:6px;display:flex;flex-direction:column;overflow:hidden;"><div style="background:linear-gradient(135deg,#e10600,#a00400);padding:6px 8px;"><div style="font-size:8px;font-weight:900;color:#fff;text-transform:uppercase;letter-spacing:0.5px;">Japanese GP</div><div style="font-size:6px;color:rgba(255,255,255,0.7);">Suzuka · 2026</div></div><div style="padding:4px 6px;display:flex;flex-direction:column;gap:2px;"><div style="display:flex;align-items:center;gap:4px;"><div style="width:14px;height:14px;border-radius:2px;background:rgba(0,188,212,0.2);color:#00bcd4;font-size:5px;display:flex;align-items:center;justify-content:center;font-weight:800;">FP1</div><div style="font-size:6px;color:#eee;">Practice 1</div><div style="font-size:6px;color:#888;margin-left:auto;">11:30</div></div><div style="display:flex;align-items:center;gap:4px;"><div style="width:14px;height:14px;border-radius:2px;background:rgba(255,214,0,0.2);color:#ffd600;font-size:5px;display:flex;align-items:center;justify-content:center;font-weight:800;">Q</div><div style="font-size:6px;color:#eee;">Qualifying</div><div style="font-size:6px;color:#888;margin-left:auto;">15:00</div></div></div></div>'
17
+ data-vis-attrs="color_accent[#e10600]/color;color_bg[#15151f]/color;color_card[#1a1a28]/color;color_text[#eeeeef]/color;color_text_muted[#666678]/color;font_size_header[22px];font_size_session[13px];font_size_time[16px];font_size_countdown[24px];timezone[Europe/Vienna];language[de];show_countdown[true]/checkbox">
18
+ <div class="vis-widget <%== this.data.attr('class') %>"
19
+ id="<%= this.data.attr('wid') %>"
20
+ style="width:440px;height:600px;overflow:hidden;">
21
+ <% vis.binds["f1"].createSessionsWidget(this.data.attr('wid'), this.view, this.data, this.style); %>
22
+ </div>
23
+ </script>