-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTvGuide.html
504 lines (391 loc) · 19.4 KB
/
TvGuide.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
<!DOCTYPE html>
<html lang="ro">
<head>
<title>TV Guide</title>
<!-- https://www.favicon-generator.org/ -->
<link rel="apple-touch-icon" sizes="57x57" href="./images/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="./images/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./images/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="./images/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="./images/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="./images/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="./images/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./images/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./images/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="./images/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="./images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="./images/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="./images/favicon/favicon-16x16.png">
<link rel="manifest" href="./images/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="./images/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes">
<!-- Prevent most search engine web crawlers from indexing a page on this site-->
<meta name="robots" content="noindex">
<meta name="color-scheme" content="light only">
<!-- Prevent browsers from caching -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" content="0"/>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/tvstyle.css?id=2021-11-19_2222">
<!-- <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> -->
<script src="js/signalr/signalr.min.js"></script>
<script src="js/jquery/jquery-3.5.1.min.js?id=2024-01-02_2222"></script>
<script src="js/tvguide.js?id=2024-04-11_2222"></script>
<style>
#divPageTitle {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
</head>
<body>
<div id="ajaxLoader"
style="visibility: visible; position: absolute;
top: 25%;
display: flex;
justify-content: center;
margin-left: auto;
margin-right: auto;
width: 100%;
">
<img alt="downloading" src="images/ajax-loader1.gif"
style="margin-left: auto;
margin-right: auto;
width: 50px;"/>
</div>
<header></header>
<div id="menuButton" class="icon" onclick="toggleMenu()">☰</div>
<div class="menu" id="menu">
<a href="#" id="btnResetCache" title="Empty Cache and Local Storage">Refresh Page</a>
<hr class="menu-divider">
<a href="#" onclick="toggleAbout()">About</a>
</div>
<div class="explanation" id="aboutDiv" style="display: none;">
<h5>TV Guide and SlingBox channel selector.</h5>
<span style="padding-bottom: 1px;">Version: <span id="tvGuideVersion"></span></span>
<hr class="menu-divider">
<div style="text-align: left; top:1px;">
<span style="font-weight:bold">Description:</span><br/>
Shows a TV Guide with channel-changing capabilities for SlingBox devices.<br/>
<hr class="menu-divider">
<span style="font-weight:bold">Copyright:</span><span> Bradut Dima, 2023 - under the MIT License</span>
<div style="margin-top: 5px">
<span style="font-weight:bold;">Source Code:</span>
<span style="margin-left:5px"><a href="https://github.com/bradut/SlingBox-TVGuide"
target="_blank">GitHub</a></span>
</div>
<hr class="menu-divider">
<div style="margin-bottom: 3px">
<span style="font-weight:bold">Trademark Disclaimer</span>
</div>
<div style="font-size: small">
All trademarks, logos and brand names are the property of their respective owners. All company, product and
service names used in this website are for identification purposes only.
</div>
</div>
<button style="border-radius: 5px;" onclick="toggleAbout()">Close</button>
</div>
<div class="jumbotron jumbotron-detail-apple-pie jumbotron-detail-mira-tv jumbotron-detail-some_picture ">
<!-- to align the title with the rest of the page which is included into a container,
include this h1 class into a container as well -->
<div id="divPageTitle" class="container text-center">
</div>
</div>
<!--local time-->
<div id="deviceTime" style="float: right; margin-right: 5px;margin-top: -25px;"></div>
<div id="divMessage" style="float: left; margin-left: 5px;margin-top: -25px;">...</div>
<div id="tvGuide" class="container-fluid grid-striped"></div>
<script id="mainScript">
/* *************************************************************************************
* Store the SignalR connection status and notifies all listeners when it changes
* Useful for components that need to know when the server is connected or disconnected
* *************************************************************************************/
class ObservableSignalRConnection {
constructor(initialSignalRConnection) {
this._value = initialSignalRConnection;
this._listeners = [];
this._oldValue = null;
}
get value() {
return this._value;
}
get oldValue() {
return this._oldValue;
}
updateOldWithNewValue() {
this._oldValue = this._value;
}
set value(newSignalRConnection) {
if (this._value === newSignalRConnection) {
return;
}
// ignore "Connecting" state
if (!isSignalRConnectionConnected(newSignalRConnection)) {
newSignalRConnection = null;
}
if (newSignalRConnection instanceof signalR.HubConnection && this._oldValue instanceof signalR.HubConnection) {
if (newSignalRConnection.state === this._value.state) {
return;
}
}
this._value = newSignalRConnection;
// notify all listeners
try {
this._listeners.forEach(listener => listener(newSignalRConnection));
} catch (e) {
console.log(e);
}
}
subscribe(listener) {
this._listeners.push(listener);
}
}
// This is the FASTEST method: display the selected buttons before the TV programs finish loading
window.onload = function () {
// Queue the function for the next available frame
requestAnimationFrame(doAfterWindowLoads);
};
let loadPageIntervalId = setInterval(loadPage, 5000);
let signalRIntervalId = setInterval(initializeSignalRConnection, 5000);
let isSlowRefresh = false;
let isAjaxLoaderVisible = true;
let signalRConnection = null;
let observableConnection = new ObservableSignalRConnection(signalRConnection);
// avoid multiple subscriptions
let isHandleSignalRConnectionChangeSubscribed = false;
const loadPageRefreshTimeOutMs = Configuration.webPageRefreshRates.loadPageTimeOutMs;
const checkSignalRConnectionTimeoutMs = Configuration.webPageRefreshRates.checkSignalRConnectionTimeoutMs;
function isSignalRConnectionConnected(signalRConnection) {
return !Utils.isNullOrUndefined(signalRConnection) &&
signalRConnection instanceof signalR.HubConnection &&
signalRConnection.state === "Connected"
}
function handleSignalRConnectionChange(newConnection) {
console.log(`${TimeService.timeStamp} SignalR Connection changed`);
// Connection changed: server has disconnected and before it was connected
if (newConnection === null && observableConnection.oldValue instanceof signalR.HubConnection) {
console.log(`${TimeService.timeStamp} SignalR Connection status: disconnected`);
observableConnection.updateOldWithNewValue();
SlingServices.displayDivChannelNumbersAsButtonsAsync();
return;
}
// Connection changed: server has connected and before it was not connected
if (observableConnection.oldValue === null) {
console.log(`${TimeService.timeStamp} SignalR Connection status: connected`);
observableConnection.updateOldWithNewValue()
SlingServices.displayDivChannelNumbersAsButtonsAsync();
reduceSignalRRefreshRate();
SlingServices.resetSignalRErrorCount();
}
}
function hideAjaxLoader() {
if (!isAjaxLoaderVisible)
return;
const ajaxLoaderElement = document.getElementById('ajaxLoader');
if (ajaxLoaderElement) {
ajaxLoaderElement.style.visibility = "hidden";
ajaxLoaderElement.style.display = "none";
isAjaxLoaderVisible = false;
}
}
function loadPage() {
console.log(`${TimeService.timeStamp} loadPage()`);
document.getElementById("deviceTime").innerHTML = TimeService.getDeviceDateTimeFormatted("dd MMM yyyy HH:mm");
if (!Configuration.isConfigurationLoaded) {
console.info(`${TimeService.timeStamp} Configuration is not loaded yet. loadPage() aborted.`);
return;
}
if (isHandleSignalRConnectionChangeSubscribed === false && Configuration.isAnySlingBoxConnected()) {
// By subscribing the handleSignalRConnectionChange function to the observableConnection,
// whenever the state of the observableConnection changes, the handleSignalRConnectionChange function
// will be called with the new state as its argument.
observableConnection.subscribe(handleSignalRConnectionChange);
console.log(`${TimeService.timeStamp} SlingBox server is configured. Initializing SignalR connection.`);
initializeSignalRConnection();
isHandleSignalRConnectionChangeSubscribed = true;
}
const deviceDateTime = TimeService.getDeviceDateTime();
const programsStartTime = TimeService.incrementDateTime(deviceDateTime, -2); // display TV programs started 2 hours ago
const tvGuide = DataServices.createTvGuide(programsStartTime);
const channelsUiCollection = DataServices.getTvChannelUiCollectionAsJson();
const htmlServices = new HtmlServices();
htmlServices.createChannelRowsIfNeeded(channelsUiCollection);
let hasTvPrograms = htmlServices.populateChannelRowsWithTvPrograms(tvGuide, deviceDateTime);
// Reduce refresh rate once we've started displaying data
if (hasTvPrograms && !isSlowRefresh) {
clearInterval(loadPageIntervalId);
setInterval(loadPage, loadPageRefreshTimeOutMs);
isSlowRefresh = true;
hideAjaxLoader();
}
}
function loadConfiguration() {
console.log(`${TimeService.timeStamp} Configuration.setUp(): Started in page`);
Configuration.setUp().then(() => {
if (Configuration.debug) console.log(`${TimeService.timeStamp} Configuration.setUp(): Completed in page`);
populatePageTitle();
}).catch((error) => {
console.error(`${TimeService.timeStamp} Configuration.setUp() failed,
error: ${error}`);
});
}
function populatePageTitle() {
const divPageTitle = document.getElementById("divPageTitle");
const templateNameUrl = HtmlUtils.appendTimeStampToUrl(Configuration.pageTitleTemplateUrl);
if (Utils.isNullOrUndefined(templateNameUrl)) {
console.error(`${TimeService.timeStamp} Configuration.pageTitleTemplateUrl is null or undefined.`);
return;
}
const localStorageKey = "pageTitleTemplate_key";
const localStorageExpirationMs = 1000 * 60 * 60 * 24 * 7; // 7 days
const pageTitleTemplate = LocalStorageServices.getDataFromLocalStorage(localStorageKey);
if (pageTitleTemplate) {
divPageTitle.innerHTML = pageTitleTemplate;
if (Configuration.debug) console.log(`${TimeService.timeStamp} Page title template loaded from local storage.`);
return;
}
fetch(templateNameUrl)
.then(function (response) {
// Throw an error if the fetch encountered any issues, force the promise to reject
if (!response.ok) {
console.error(`${TimeService.timeStamp} Could not read template file: ${templateNameUrl}`);
HtmlServices.displayMessage("Error reading title", HtmlServices.messageTypeError);
throw new Error(`HTTP error! status: ${response.status}`);
}
// convert the response body into a text string
// the subsequent .then in the promise chain will receive the response body as text.
return response.text();
})
.then(function (htmlString) {
// This is the HTML from our response as a text string
divPageTitle.innerHTML = HtmlUtils.removeComments(htmlString);
if (Configuration.debug) console.log(`${TimeService.timeStamp} Page title template loaded from the config file.`);
LocalStorageServices.saveDataToLocalStorage(localStorageKey, divPageTitle.innerHTML, localStorageExpirationMs);
})
.catch(function (err) {
console.error(`${TimeService.timeStamp} Could not read template file: ${templateNameUrl}`, err);
});
}
function doAfterWindowLoads() {
/*
The "requestAnimationFrame" API allows running a function in the background to apply transformations
without introducing arbitrary delays or affecting the page's responsiveness.
*/
requestAnimationFrame(async function () {
loadConfiguration();
HtmlServices.displayMessage("Version: " + Configuration.tvGuideVersion);
});
}
// reduce refresh rate once we've started displaying data
function reduceSignalRRefreshRate() {
if (isSignalRConnectionConnected(observableConnection.value)) {
clearInterval(signalRIntervalId);
setInterval(initializeSignalRConnection, checkSignalRConnectionTimeoutMs);
}
}
function initializeSignalRConnection() {
if (!Configuration.isConfigurationLoaded) {
console.log(`${TimeService.timeStamp} Configuration is not loaded yet. initializeSignalRConnection() aborted.`);
return;
}
if (!Configuration.isAnySlingBoxConnected()) {
if (Configuration.debug) console.log(`${TimeService.timeStamp} initializeSignalRConnection() - SlingBox server is not configured or not connected.`);
return;
}
if (!Configuration.isPageLaunchedFromSlingBoxServer) {
console.error(`${TimeService.timeStamp} Page is not launched from SlingBox Server. SignalR connection not initialized.`)
return;
}
if (SlingServices.cannotConnectToSignalR()) {
console.error(`${TimeService.timeStamp} SignalR connection error count reached ${SlingServices.maxSignalRErrorCount}. SignalR connection not initialized.`);
return;
}
const connection = SlingServices.initializeSignalRConnection();
if (isSignalRConnectionConnected(connection)) {
observableConnection.value = connection;
} else {
observableConnection.value = null;
}
}
function toggleMenu() {
const menu = document.querySelector('.menu');
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
}
function toggleAbout() {
const aboutDiv = document.getElementById('aboutDiv');
aboutDiv.style.display = aboutDiv.style.display === 'block' ? 'none' : 'block';
}
// Close the menu when clicking anywhere on the page
document.body.addEventListener('click', (event) => {
// Get references to your button and menu
const menuButton = document.getElementById('menuButton');
const menu = document.querySelector('.menu');
const aboutDiv = document.getElementById('aboutDiv');
// Check if the click event target is not the menu button or the menu itself
if (event.target !== menuButton && !menu.contains(event.target)) {
const menu = document.querySelector('.menu');
menu.style.display = 'none'; // Close the menu
aboutDiv.style.display = 'none'; // Close the about div
}
});
function closeMenu() {
const menu = document.querySelector('.menu');
menu.style.display = 'none';
}
function populateAboutWithAppVersion() {
const tvGuideVersion = document.getElementById('tvGuideVersion');
tvGuideVersion.innerHTML = Configuration.tvGuideVersion;
}
document.addEventListener("DOMContentLoaded", function () {
// This function will execute when the DOM is fully loaded
populateAboutWithAppVersion();
});
document.getElementById("btnResetCache").addEventListener("click", function (e) {
e.preventDefault(); // Prevent the default behavior of the link
Utils.clearLocalStorageAndRefreshPage();
HtmlServices.displayMessage("Local storage has been reset.", HtmlServices.messageTypeWarning);
closeMenu();
});
</script>
<!-- The slim version of jquery doesn't have AJAX -->
<!-- <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script> -->
<script src="js/bootstrap.js"></script>
<!-- modal dialog -->
<div id="displayProgramsModal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Some Title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<!-- <p>Some text here</p> -->
<div id="divDateTimeModal" class="h6" style="text-align: right;">1 Sept.2020 15:40:05</div>
<div id="divTvProgramModal"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary btn-lg p-0"
data-dismiss="modal">
OK
</button>
</div>
</div>
</div>
</div>
<script>
// handle the modal popup
$('#displayProgramsModal')
.on('show.bs.modal', HtmlServices.populateModalPopup)
.on('hide.bs.modal', HtmlServices.disposeModalPopup);
</script>
</body>
</html>