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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/admin/f1.png +0 -0
- package/admin/f1.svg +43 -0
- package/admin/i18n/de.json +24 -0
- package/admin/i18n/en.json +24 -0
- package/admin/i18n/es.json +24 -0
- package/admin/i18n/fr.json +24 -0
- package/admin/i18n/it.json +24 -0
- package/admin/i18n/nl.json +24 -0
- package/admin/i18n/pl.json +24 -0
- package/admin/i18n/pt.json +24 -0
- package/admin/i18n/ru.json +24 -0
- package/admin/i18n/uk.json +24 -0
- package/admin/i18n/zh-cn.json +24 -0
- package/admin/jsonConfig.json +75 -0
- package/build/main.js +1028 -0
- package/build/main.js.map +1 -0
- package/io-package.json +150 -0
- package/package.json +77 -0
- package/widgets/f1-sessions/css/f1-sessions.css +14 -0
- package/widgets/f1-sessions/js/f1-sessions.js +450 -0
- package/widgets/f1.html +23 -0
|
@@ -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
|
+
}());
|
package/widgets/f1.html
ADDED
|
@@ -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>
|