jekyll-theme-conference 2.5.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -48
  3. data/_includes/js/conference-live.js +463 -151
  4. data/_includes/js/conference-program.js +48 -0
  5. data/_includes/js/conference.js +11 -11
  6. data/_includes/js/jquery-3.5.1.min.js +2 -0
  7. data/_includes/partials/checks.html +66 -31
  8. data/_includes/partials/footer.html +5 -4
  9. data/_includes/partials/get_day_hash.html +20 -0
  10. data/_includes/partials/get_day_time.html +19 -0
  11. data/_includes/partials/get_link.html +43 -43
  12. data/_includes/partials/get_link_types.html +14 -14
  13. data/_includes/partials/get_live_timestamps.html +49 -0
  14. data/_includes/partials/get_main_category.html +9 -9
  15. data/_includes/partials/get_talk_time.html +7 -7
  16. data/_includes/partials/get_talk_timestamp.html +4 -0
  17. data/_includes/partials/get_time_pronoun.html +6 -0
  18. data/_includes/partials/header.html +22 -6
  19. data/_includes/partials/info_bar.html +22 -22
  20. data/_includes/partials/list_categories.html +2 -2
  21. data/_includes/partials/list_speakers.html +14 -18
  22. data/_includes/partials/list_sub_categories.html +7 -7
  23. data/_includes/partials/{live-modal.html → live_modal.html} +24 -5
  24. data/_includes/partials/navbar.html +28 -35
  25. data/_includes/partials/navbar_rooms.html +21 -20
  26. data/_includes/partials/show_live_button.html +17 -0
  27. data/_includes/partials/show_room.html +5 -5
  28. data/_includes/partials/show_talk.html +3 -3
  29. data/_includes/partials/show_talk_duration.html +1 -1
  30. data/_includes/partials/show_talk_time.html +16 -2
  31. data/_layouts/data.html +91 -0
  32. data/_layouts/home.html +48 -6
  33. data/_layouts/program.html +179 -148
  34. data/_layouts/room.html +50 -43
  35. data/_layouts/speaker.html +46 -42
  36. data/_layouts/talk-overview.html +83 -53
  37. data/_layouts/talk.html +43 -42
  38. data/_sass/conference.scss +29 -12
  39. data/assets/icons/live.svg +33 -57
  40. data/assets/js/data.json +3 -0
  41. data/assets/js/main.js +0 -6
  42. metadata +13 -7
  43. data/_includes/js/jquery-3.2.1.slim.min.js +0 -4
  44. data/_includes/partials/get_conf_time.html +0 -54
  45. data/_includes/partials/get_timestamp.html +0 -4
  46. data/_includes/partials/live_button.html +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85035d6385b1339b58d26c9aa7656bb63c390e82e59b995a954b032bda281b61
4
- data.tar.gz: 6cde36017cc9d2b667ea921edcb6ed8938fea8cf63b87169f2ea8399b0e42b53
3
+ metadata.gz: b4319ed637aa4734e12cd0d9eef3a807f314e8a9e4c0ff3a79b8781e8e00ebb9
4
+ data.tar.gz: d0c52c4aa84e87fb5c9892b6509db3de79a640e2e3ad005359febda835c412be
5
5
  SHA512:
6
- metadata.gz: 6ee86daa817de1f8ce650b73c8a1116a28293307da1cfe10abd775331251d88af1a33e6dbc425011cf5d4358e89978e2601cf5eaf35c22874222715f170c0a88
7
- data.tar.gz: 93606208e131a2d476732a828248cbdc7d58cf28ed0e6e678a585d677e18b6939721a6ca099a1f7805e0b431e2585989168aed5e45f009f7d35780b498d615ba
6
+ metadata.gz: 849e5facbe864d96c63fcdcd218ee4a4a4d8e63a9787aca58c10b1fafdf20704ce3d529caa519940ae65bd3a561ffc6127f2da243a823e676604b538e2f24c35
7
+ data.tar.gz: 5bca6a3b8624e8e8f04797292cc969dd2f995fb74ffedd178dee2d59b0410cd93d1dd5479d8c64cdc99728ca7160e05c31e9537a93866093ed88bd65142e0a03
data/README.md CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  ![Screenshot](screenshot.png)
4
4
 
5
- This is a [Jekyll](http://jekyllrb.com) theme based on [Bootstrap 4](http://getbootstrap.com) which can be used to make a simple, responsive website for a one-day conference or workshop with parallel tracks containing:
5
+ This is a responsive [Jekyll](http://jekyllrb.com) theme based on [Bootstrap 4](http://getbootstrap.com) for conferences. It contains
6
6
 
7
- - program / schedule
8
- - talk and speaker descriptions
9
- - map for directions
7
+ - multiday program / schedule,
8
+ - talk and speaker descriptions,
9
+ - map for directions,
10
+ - realtime live indications during the conference, and
11
+ - supports embedded video streaming or recordings.
10
12
 
11
- All components such as talks, speakers or rooms are represented as collection of files. The schedule is given is defined via a simple structure stored in a [YAML](https://en.wikipedia.org/wiki/YAML) file. There is no need for databases and once generated the website consists only of static files.
12
- The design is easily modifiable and is adapted for mobile uses and printing.
13
+ All components such as talks, speakers or rooms are represented as collection of files. The schedule is given is defined via a simple structure stored in a [YAML](https://en.wikipedia.org/wiki/YAML) file. There is no need for databases and once generated the website consists only of static files. A script and workflows are available for easy import, e.g. of [frab](https://github.com/frab/frab/wiki/Manual#introduction) compatible schedules.
14
+ The design is easily customizable and is adapted for mobile uses and printing.
13
15
 
14
16
  The theme was originally created for the yearly Winterkongress conference of the [Digital Society Switzerland](https://digitale-gesellschaft.ch/). You can see this theme in action here:
15
17
 
@@ -44,11 +46,7 @@ This allows for easier installation and updating as you don't have to manage any
44
46
  theme: jekyll-theme-conference
45
47
  ```
46
48
 
47
- 4. Copy the internationalization file containing the different language strings for this repository to your Jekyll folder:
48
-
49
- - `_data/lang.yml`
50
-
51
- 5. Continue with the _Setup_ section further below to customize the theme and add some content for your conference
49
+ 4. Continue with the _Setup_ section further below to customize the theme and add some content for your conference
52
50
 
53
51
  To update the theme run `bundle update`.
54
52
 
@@ -76,11 +74,7 @@ To install:
76
74
 
77
75
  4. Add `remote_theme: "DigitaleGesellschaft/jekyll-theme-conference@2.0.0"` to your `_config.yml` file. Remove any other `theme:` or `remote_theme:` entry.
78
76
 
79
- 5. Copy the internationalization file containing the different language strings for this repository to your Jekyll folder:
80
-
81
- - `_data/lang.yml`
82
-
83
- 6. Continue with the _Setup_ section further below to customize the theme and add some content for your conference
77
+ 5. Continue with the _Setup_ section further below to customize the theme and add some content for your conference
84
78
 
85
79
 
86
80
  ## Setup
@@ -89,6 +83,10 @@ The different talks, speakers and rooms are stored as a collection of files. Eac
89
83
 
90
84
  The actual schedule defining when and in which room a talk takes place is stored as a [YAML data file](https://jekyllrb.com/docs/datafiles/) under `_data/program.yml`. For further details about it see below in the section _Content_.
91
85
 
86
+ :warning: Please note that the generated website can be quite large containing many unnecessary whitespaces. It is recommended to minimize the generated output files before uploading them to a server (e.g. with [minify](https://github.com/tdewolff/minify)).
87
+
88
+ ### Jump Start
89
+
92
90
  In order to be up and running simply use the default content of this repository as an initial base for your new website. After having setup a new Jekyll website copy the following files and folders into the website's folder:
93
91
 
94
92
  - `_config.example.yml` -> `_config.yml`
@@ -102,11 +100,43 @@ In order to be up and running simply use the default content of this repository
102
100
  - `speakers/`
103
101
  - `talks/`
104
102
 
105
- There exists a Python file in this repository, `create_entries.py`, which can be used to import content from a CSV table and generate the different talk, speakers and room files automatically based on it. Just open your terminal and type `python create_entries.py --help` to show the help and get started.
103
+ ### Automatic Import
104
+
105
+ There exists a Python file in this repository, `_tools/create_entries.py`, which can be used to import content from a [frab](https://github.com/frab/frab/wiki/Manual#introduction) compatible JSON file (e.g. from [pretalx.com](https://pretalx.com/p/about/)) or a CSV table and generate the different talk, speakers and room files automatically. It has as only dependency [PyYAML](https://pypi.org/project/PyYAML/):
106
+
107
+ 1. Copy the file `_tools/create_entries.py` from this repository
106
108
 
107
- In case you do not want to install the entire Ruby/Jekyll toolchain on your machine you can make use of [GitHub Actions](https://github.com/features/actions), Github's continuous integration platform. This repository contains an example Github Action configuration file which automatically builds the website upon adding a new tag. It then attaches the generated website as package to a release for easy downloading. Simply copy the following file to your repository and adapt it to your needs:
109
+ 2. Create a virtual environment and activate it
108
110
 
109
- - `workflows.example.yml` -> `.github/workflows.main.yml`
111
+ ```bash
112
+ python -m venv venv
113
+ source venv/bin/activate
114
+ ```
115
+
116
+ 3. Install PyYAML
117
+
118
+ ```bash
119
+ pip install pyyaml
120
+ ```
121
+
122
+ 4. Execute the script, e.g. to show the help type
123
+
124
+ ```bash
125
+ python _tools/create_entries.py --help
126
+ ```
127
+
128
+
129
+ ### Automatic Build
130
+
131
+ In case you do not want to install the entire Ruby/Jekyll toolchain on your machine you can make use of [GitHub Actions](https://github.com/features/actions), Github's continuous integration platform. This repository contains multiple example Github Action configuration files in the `_tools/` folder:
132
+
133
+ - `build.yml`: automatically builds and minimizes the website upon adding a new tag starting with a `v` (e.g. `v2020.01.01`). It then attaches the generated website as an archive to a release for easy downloading.
134
+ - `test.yml`: automatically tries to build the website upon a new pull request. It can thus be used as status check before merging.
135
+ - `schedule.yml`: automatically generates the schedule and content files when a new pull request contains a `schedule.json` file (see the _Automatic Import_subsection above). Thus, it allows quick updates of the site's content from [pretalx.com](https://pretalx.com/p/about/) exports.
136
+
137
+ To get started, simply copy the desired workflow file to your repository and adapt it to your needs:
138
+
139
+ - `_tools/build.yml` -> `.github/workflows/build.yml`
110
140
 
111
141
 
112
142
  ## Configuration
@@ -124,6 +154,8 @@ conference:
124
154
  show_errors: false
125
155
  ```
126
156
 
157
+ :warning: Please be sure to disable this parameter for your production system.
158
+
127
159
  ### Collection URLs
128
160
 
129
161
  The three required collections containing the files for the talks, speakers and rooms have to be specified in the `_config.yml` file. The first block declares them and sets the URL under which they will later be accessed. The second block defines the default layout for each of the collection.
@@ -183,11 +215,6 @@ conference:
183
215
 
184
216
  The navigation bar is located at the top and visible on every site. On the right it show the title of the website (`site.title`) followed by the links listed under the `links` property of the `navigation` property. See the _Content_ > _Links_ section below for the available properties per link.
185
217
 
186
- Additionally, a navigation bar link can also have the following properties:
187
-
188
- - `menu` containing another list of links. This creates a dropdown menu of multiple sublinks. The sublinks have the same properties as regular links (see the _Content_ > _Links_ section), or
189
- - `live` making the link only visible during the conference and adds a live indication. The `name` property can be omitted. If streaming is enabled and any URL property is omitted, a click on the link will open the streaming modal (see section _Live Indications_ below).
190
-
191
218
  Example:
192
219
 
193
220
  ```yaml
@@ -294,21 +321,23 @@ In order to help users navigating the program during the congress, a _Live_ indi
294
321
 
295
322
  This can be further extended if some of the talks have an associated live stream: Upon clicking one of the live indications a modal will open containing the corresponding live stream embedded. The URL to the live stream has to be set via `live` property in each room (see the _Content_ > _Room_ section below).
296
323
 
297
- In order to activate the functionality the `live` property has to be set containing
324
+ In order to activate the functionality, each day in the `program.yml` file must contain a `date` property (see section _Content_ > _Schedule / Program_ below) and the `live` property has to be set in the configuration file containing
298
325
 
299
- - the date of the day at which the conference takes place (`date`),
300
- - the timezone in which the conference takes place (`timezone`),
301
- - optionally if streaming is enabled (`streaming`) with indications how many minutes early the stream will begin and end, and
326
+ - how long a pause between two consecutive talks has to be for the live indication to pause (`time_stop`),
327
+ - optionally if streaming is enabled (`streaming`) with indications
328
+ + how many minutes the stream goes active before a talk (`time_prepend`),
329
+ + how many minutes the stream stays active after a talk (`time_extend`), and
330
+ + how long a pause between two consecutive talks has to be for the stream to pause (`time_pause`),
302
331
  - optionally a demo mode setting, whereby the JavaScript function cycles through the entire program in five minutes for demonstration purposes (`demo: true`, default: `false`).
303
332
 
304
333
  ```yaml
305
334
  conference:
306
335
  live:
307
- date: 01.01.2020
308
- timezone: GMT+1
336
+ time_stop: 240 # in minutes
309
337
  streaming:
310
- start_early: 15 # in minutes
311
- end_late: 0 # in minutes
338
+ time_pause: 60 # in minutes
339
+ time_prepend: 5 # in minutes
340
+ time_extend: 5 # in minutes
312
341
  demo: false
313
342
  ```
314
343
 
@@ -430,33 +459,52 @@ The actual schedule defining when and in which room a talk takes place is stored
430
459
 
431
460
  ### Schedule / Program
432
461
 
433
- The schedule of the conference linking the talks with the rooms and indicating when each talk talks place and how long it goes is set in the `_data/program.yml` file. It consists of an array of rooms each consisting of a
462
+ The schedule of the conference linking the talks with the rooms and indicating when each talk talks place and how long it goes is set in the `_data/program.yml` file. It contains a list of days, whereby each day contains a list of rooms, whereby each room contains a list of talks.
463
+
464
+ Each day consists of
434
465
 
435
- - `room` name (must correspond to one of the room identifier), and
436
- - an array of `talks` which also can be empty `[]`.
466
+ - a list of rooms (`rooms`) in which talks are taking place on that day
467
+ - optionally, the day's `name`, e.g. the weekday
468
+ - optionally, the short form of the day's name (`abbr`), and
469
+ - optionally only if no live indications are active, a `date` in the format `YYYY-MM-DD`.
437
470
 
438
- The order of the room in the file defines the order of the rooms on the website (program and room listings). Each talk in the array consists of
471
+ Each room consists of
472
+
473
+ - the room's `name` (must correspond to one of the room identifier), and
474
+ - a list of talks (`talks`) which also can be empty `[]`.
475
+
476
+ The order of the rooms in the list defines the order of the rooms as shown in the schedule on the program page. For the live streaming or the room overview the order of the rooms is alphabetical but can be adapted via the [main configuration file](https://jekyllrb.com/docs/collections/#sort-by-front-matter-key).
477
+
478
+ Each talk consists of
439
479
 
440
480
  - a `name` (must correspond to one of the talk identifier),
441
481
  - a starting time `time_start` given as `H:M` ([`strftime`](http://www.strfti.me) formated), and
442
482
  - an end time `time_end`.
443
483
 
444
- The array should (manually) be ordered by time. Currently talks can only take place on the same day and multi-day conferences are not supported.
484
+ The list of talks should (manually) be ordered by time, i.e. the first occurring talk should be listed first.
445
485
 
446
486
  Example:
447
487
 
448
488
  ```yaml
449
- - room: Room A
450
- talks:
451
- - name: Vim Impetus Placerat Cotidieque Ad
452
- time_start: '12:00'
453
- time_end: '12:45'
454
- - name: Condimentum Vitae Sapien Pellentesque
455
- time_start: '12:45'
456
- time_end: '13:30'
457
- - room: Room B
458
- talks:
459
- ...
489
+ days:
490
+ - name: Monday
491
+ abbr: Mo
492
+ date: 2020-01-31
493
+ rooms:
494
+ - name: Room A
495
+ talks:
496
+ - name: Vim Impetus Placerat Cotidieque Ad
497
+ time_start: '12:00'
498
+ time_end: '12:45'
499
+ - name: Condimentum Vitae Sapien Pellentesque
500
+ time_start: '12:45'
501
+ time_end: '13:30'
502
+
503
+ - name: Room B
504
+ talks:
505
+ - name: Arcu Non Odio
506
+ time_start: '12:00'
507
+ time_end: '13:00'
460
508
  ```
461
509
 
462
510
  ### Talks
@@ -501,6 +549,11 @@ Links are used at different location throughout the theme: They can either be us
501
549
  + pointing to a file uploaded to the `/documents` folder (for talks `/documents/slides`, for speakers `/documents/bio`): `file:`
502
550
  + pointing to an external video: `video:`
503
551
 
552
+ Additionally, a navigation bar or main landing page link can also have the following properties:
553
+
554
+ - `menu` containing another list of links. This creates a dropdown menu of multiple sublinks. The sublinks have the same properties as regular links, or
555
+ - `live` making the link only visible during the conference and adds a live indication. The `name` property can be omitted. If streaming is enabled and any URL property is omitted, a click on the link will open the streaming modal (see section _Live Indications_ above).
556
+
504
557
  Using the `file:` indicator, the relative address is automatically set as well as the icon. Using the `video:` indicator, the link is automatically configured to open in an iframe with a corresponding title and the icon is set.
505
558
 
506
559
  Example:
@@ -1,11 +1,21 @@
1
1
  window.conference.live = (function() {
2
- {%- include partials/get_conf_time.html -%}
3
- {%- assign time_start = conf_start -%}
4
- {%- assign time_end = conf_end -%}
5
- {%- include partials/get_timestamp.html -%}
2
+ {% assign d = site.data.program.days | first -%}
3
+ {%- include partials/get_day_time.html -%}
4
+ {%- assign t = day_start_talk -%}
6
5
 
7
- let confStart = {{ timestamp_start }};
8
- let confEnd = {{ timestamp_end }};
6
+ {%- include partials/get_talk_timestamp.html -%}
7
+ {%- assign conf_start = timestamp_start -%}
8
+
9
+ {%- assign d = site.data.program.days | last -%}
10
+ {%- include partials/get_day_time.html -%}
11
+ {%- assign t = day_end_talk -%}
12
+
13
+ {%- include partials/get_talk_timestamp.html -%}
14
+ {%- assign conf_end = timestamp_end -%}
15
+
16
+ let confStart = {{ conf_start }};
17
+ let confEnd = {{ conf_end }};
18
+ let confDur = confEnd - confStart;
9
19
 
10
20
  let freezeTime = false;
11
21
  let timeFrozen = 0;
@@ -15,250 +25,509 @@ window.conference.live = (function() {
15
25
  let durDemo = 5*60; // in seconds
16
26
  let durPause = 10; // in seconds
17
27
 
28
+ let demoStart = confStart - confDur/durDemo*durPause;
29
+ let demoEnd = confEnd + confDur/durDemo*durPause;
30
+
18
31
  let liveTimer;
19
- let liveTimerCorr;
32
+ let streamVideoTimer;
33
+ let streamInfoTimer;
34
+
35
+ let mod = function (n, m) {
36
+ return ((n % m) + m) % m;
37
+ };
20
38
 
21
- let timeNowReal = function () {
22
- // Return UNIX timestamp in seconds
39
+ let timeNow = function () {
23
40
  return Math.floor(Date.now() / 1000);
24
41
  };
25
42
 
26
- let timeNowCycle = function() {
27
- // Cycle time over program for a fixed duration
28
- let relTime = (Math.floor(Date.now() / 1000) % durDemo - durPause) / (durDemo - 2*durPause);
29
- let cycleTime = (confEnd - confStart) * relTime + confStart;
43
+ let timeCont = function () {
44
+ return timeNow() - timeOffset;
45
+ };
46
+
47
+ let timeCycle = function () {
48
+ let actTime = timeNow();
49
+ let relTime = mod(actTime, durDemo + 2*durPause) / durDemo;
50
+ let cycleTime = mod((demoEnd - demoStart) * relTime - timeOffset, (demoEnd - demoStart)) + demoStart;
30
51
  return cycleTime;
31
52
  };
32
53
 
33
- let timeNow = function() {
54
+ let time = function () {
34
55
  if (freezeTime) {
35
56
  return timeFrozen;
36
57
  }
37
58
  else if (demo) {
38
- return timeNowCycle() - timeOffset;
59
+ return timeCycle();
39
60
  }
40
61
  else {
41
- return timeNowReal() - timeOffset;
62
+ return timeCont();
42
63
  }
43
64
  };
44
65
 
45
66
  let pauseTime = function () {
46
- timeFrozen = timeNow();
47
- freezeTime = true;
67
+ if (!freezeTime) {
68
+ timeFrozen = time();
69
+ freezeTime = true;
70
+
71
+ stopUpdate();
72
+ }
48
73
  };
49
74
 
50
75
  let continueTime = function () {
51
- if (demo) {
52
- timeOffset = timeNowCycle() - timeFrozen;
53
- }
54
- else {
55
- timeOffset = timeNow() - timeFrozen;
76
+ if (freezeTime) {
77
+ freezeTime = false;
78
+ timeOffset += time() - timeFrozen;
79
+ startUpdate();
56
80
  }
57
- freezeTime = false;
58
81
  };
59
82
 
60
83
  let resetTime = function (timeStr) {
61
- continueTime();
62
84
  timeOffset = 0;
85
+ freezeTime = false;
86
+
87
+ startUpdate();
63
88
  };
64
89
 
65
- let setTime = function (timeStr) {
90
+ let setTime = function (newTime, newDay=1) {
66
91
  pauseTime();
67
92
 
68
- let d = new Date(timeNow() * 1000);
69
- time = timeStr.split(':');
70
- d.setHours(time[0], time[1]);
93
+ let dayIdx;
94
+ if (Number.isInteger(newDay)) {
95
+ dayIdx = newDay-1;
96
+ }
97
+ else if (/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(newDay)) {
98
+ dayIdx = data.days.find(o => o.name === newDay);
99
+ }
100
+ else {
101
+ dayIdx = data.days.find(o => o.name === newDay);
102
+ }
103
+ newDate = data.days[dayIdx].date;
104
+
105
+ let d = new Date(newDate);
106
+ newTime = newTime.split(':');
107
+ d.setHours(newTime[0], newTime[1]);
71
108
 
72
109
  timeFrozen = Math.floor(d.getTime() / 1000);
110
+
111
+ update();
73
112
  };
74
113
 
75
- let getTime = function () {
76
- let d = new Date(timeNow() * 1000);
114
+ let getTime = function (tConvert=time()) {
115
+ let d = new Date(tConvert * 1000);
116
+ let dStr = d.toISOString().slice(0,10);
77
117
  let h = d.getHours();
78
118
  let m = d.getMinutes();
79
119
 
80
- return h + ":" + (m < 10 ? "0" : "") + m;
120
+ return dStr +" "+ h +":"+ (m < 10 ? "0" : "") + m;
81
121
  };
82
122
 
83
- let timeStart = function () {
84
- let tNow = timeNow();
85
- if (confStart - 60 > tNow) {
86
- // Start when conference start (-60s)
87
- return confStart - 60 - tNow;
123
+ let timeUnit = function () {
124
+ if (demo) {
125
+ return 0.1;
88
126
  }
89
127
  else {
90
- // Start on the minute
91
- return (60 - (tNow % 60));
128
+ return 60;
92
129
  }
93
130
  };
94
131
 
95
- let updateLiveButtons = function() {
96
- let tNow = timeNow();
132
+ let delayStart = function (startTime) {
133
+ let tNow = time();
134
+ let tUnit = timeUnit();
135
+
136
+ if (demo) {
137
+ // Convert virtual duration to real duration
138
+ return mod(startTime - tNow, demoEnd - demoStart) / (demoEnd - demoStart) * (durDemo + 2*durPause);
139
+ }
140
+ else {
141
+ if (startTime > tNow) {
142
+ return startTime - tNow;
143
+ }
144
+ else {
145
+ // Start on the unit
146
+ return (tUnit - (tNow % tUnit));
147
+ }
148
+ }
149
+ };
150
+
151
+ let toggleDemo = function () {
152
+ demo = !demo;
153
+ resetTime();
154
+ };
155
+
156
+ let demoOn = function () {
157
+ return demo;
158
+ };
159
+
160
+ let updateLive = function () {
161
+ let tNow = time();
97
162
  let liveShow = document.getElementsByClassName('live-show');
98
163
  let liveHide = document.getElementsByClassName('live-hide');
164
+ let liveTime = document.getElementsByClassName('live-time');
165
+ let livePast = document.getElementsByClassName('live-past');
99
166
 
100
167
  for (let i = 0; i < liveShow.length; i++) {
101
- let tStart = liveShow[i].dataset.start;
102
- let tEnd = liveShow[i].dataset.end;
168
+ let tStarts = liveShow[i].dataset.start.split(',');
169
+ let tEnds = liveShow[i].dataset.end.split(',');
170
+
171
+ for (let k = 0; k < tStarts.length; k++) {
172
+ if (tNow >= tStarts[k] && tNow < tEnds[k]) {
173
+ // Show when active
174
+ liveShow[i].classList.remove('d-none');
175
+ break;
176
+ }
177
+ else if (!liveShow[i].classList.contains('d-none')) {
178
+ // Hide otherwise
179
+ liveShow[i].classList.add('d-none');
180
+ }
181
+ }
182
+ }
103
183
 
104
- if (tNow >= tStart && tNow < tEnd) {
105
- // Show when active
106
- liveShow[i].classList.remove('d-none');
184
+ for (let i = 0; i < liveHide.length; i++) {
185
+ let tStarts = liveHide[i].dataset.start.split(',');
186
+ let tEnds = liveHide[i].dataset.end.split(',');
187
+
188
+ for (let k = 0; k < tStarts.length; k++) {
189
+ if (tNow >= tStarts[k] && tNow < tEnds[k]) {
190
+ // Hide when active
191
+ if (!liveHide[i].classList.contains('d-none')) {
192
+ liveHide[i].classList.add('d-none');
193
+ break;
194
+ }
195
+ }
196
+ else {
197
+ // Show otherwise
198
+ liveHide[i].classList.remove('d-none');
199
+ }
107
200
  }
108
- else if (!liveShow[i].classList.contains('d-none')) {
109
- // Hide otherwise
110
- liveShow[i].classList.add('d-none');
201
+ }
202
+
203
+ for (let i = 0; i < liveTime.length; i++) {
204
+ let t = liveTime[i].dataset.time;
205
+ if (typeof t == "undefined") {
206
+ break;
207
+ }
208
+ let tRel = tNow - t;
209
+
210
+ let tStr;
211
+ if (tRel >= -60 && tRel < 0) {
212
+ tStr = '{{ site.data.lang[site.conference.lang].live.time.soon | default: "soon" }}';
213
+ }
214
+ else if (tRel >= 0 && tRel < 60) {
215
+ tStr = '{{ site.data.lang[site.conference.lang].live.time.now | default: "now" }}';
216
+ }
217
+ else {
218
+ if (tRel < 0) {
219
+ tStr = '{{ site.data.lang[site.conference.lang].live.time.in | default: "in" }} ';
220
+ }
221
+ else {
222
+ tStr = '{{ site.data.lang[site.conference.lang].live.time.since | default: "since" }} ';
223
+ }
224
+ tRel = Math.abs(tRel);
225
+
226
+ let dWeeks = Math.floor(tRel / (7*24*60*60));
227
+ let dDays = Math.floor(tRel / (24*60*60));
228
+ let dHours = Math.floor(tRel / (60*60));
229
+ let dMins = Math.floor(tRel / (60));
230
+ if (dWeeks > 4) {
231
+ break;
232
+ }
233
+ else if (dWeeks > 1) {
234
+ tStr += dWeeks +' {{ site.data.lang[site.conference.lang].live.time.weeks | default: "weeks" }}';
235
+ }
236
+ else if (dWeeks == 1) {
237
+ tStr += '1 {{ site.data.lang[site.conference.lang].live.time.week | default: "week" }}';
238
+ }
239
+ else if (dDays > 1) {
240
+ tStr += dDays +' {{ site.data.lang[site.conference.lang].live.time.days | default: "days" }}';
241
+ }
242
+ else if (dDays == 1) {
243
+ tStr += '1 {{ site.data.lang[site.conference.lang].live.time.day | default: "day" }}';
244
+ }
245
+ else if (dHours > 1) {
246
+ tStr += dHours +' {{ site.data.lang[site.conference.lang].live.time.hours | default: "hours" }}';
247
+ }
248
+ else if (dHours == 1) {
249
+ tStr += '1 {{ site.data.lang[site.conference.lang].live.time.hour | default: "hour" }}';
250
+ }
251
+ else if (dMins > 1) {
252
+ tStr += dMins +' {{ site.data.lang[site.conference.lang].live.time.minutes | default: "minutes" }}';
253
+ }
254
+ else {
255
+ tStr += '1 {{ site.data.lang[site.conference.lang].live.time.minute | default: "minute" }}';
256
+ }
111
257
  }
258
+
259
+ liveTime[i].innerHTML = tStr;
112
260
  }
113
261
 
114
- for (let i = 0; i < liveHide.length; i++) {
115
- let tStart = liveHide[i].dataset.start;
116
- let tEnd = liveHide[i].dataset.end;
262
+ for (let i = 0; i < livePast.length; i++) {
263
+ let t = livePast[i].dataset.time;
264
+ if (typeof t == "undefined") {
265
+ break;
266
+ }
267
+ let tRel = tNow - t;
117
268
 
118
- if (tNow >= tStart && tNow < tEnd) {
119
- // Hide when active
120
- if (!liveHide[i].classList.contains('d-none')) {
121
- liveHide[i].classList.add('d-none');
269
+ if (tRel < 0) {
270
+ // Grey out when in past
271
+ if (!livePast[i].classList.contains('text-secondary')) {
272
+ livePast[i].classList.add('text-secondary');
122
273
  }
123
274
  }
124
275
  else {
125
- // Show otherwise
126
- liveHide[i].classList.remove('d-none');
276
+ // Show normal otherwise
277
+ livePast[i].classList.remove('text-secondary');
127
278
  }
128
279
  }
129
280
 
130
- if (timeNow() > confEnd && !demo) {
281
+ if (tNow > confEnd && !demo) {
131
282
  // Cancel timer after program is over
132
- clearInterval(liveTimer);
283
+ stopUpdateLive();
133
284
  }
134
285
  };
135
286
 
136
- let startUpdate = function () {
137
- if(typeof liveTimer !== "undefined") {
138
- clearInterval(liveTimer);
139
- }
140
- updateLiveButtons();
287
+ let startUpdateLive = function () {
288
+ stopUpdateLive();
289
+ updateLive();
141
290
 
142
291
  if (demo) {
143
- liveTimerCorr = (confEnd - confStart) / (durDemo - 2*durPause);
144
- liveTimer = setInterval(updateLiveButtons, 100);
292
+ // Immediate start required since delayStart would wait for next wrap around
293
+ liveTimer = setInterval(updateLive, timeUnit() * 1000);
145
294
  }
146
295
  else {
147
- liveTimerCorr = 1;
148
296
  setTimeout(function() {
149
- liveTimer = setInterval(updateLiveButtons, 60*1000);
150
- updateLiveButtons();
151
- }, timeStart() * 1000);
297
+ liveTimer = setInterval(updateLive, timeUnit() * 1000);
298
+ updateLive();
299
+ }, delayStart(confStart) * 1000);
152
300
  }
153
301
  };
154
302
 
155
- let toggleDemo = function () {
156
- demo = !demo;
157
- timeOffset = 0;
158
- startUpdate();
159
- };
160
-
161
- let demoOn = function() {
162
- return demo;
303
+ let stopUpdateLive = function () {
304
+ if (typeof liveTimer !== "undefined") {
305
+ clearInterval(liveTimer);
306
+ }
163
307
  };
164
308
 
165
309
  {% if site.conference.live.streaming -%}
310
+ let streamPause = {{ site.conference.live.streaming.time_pause | default: 60 }}; // in minutes
311
+ let streamPrepend = {{ site.conference.live.streaming.time_prepend | default: 5 }}; // in minutes
312
+ let streamExtend = {{ site.conference.live.streaming.time_extend | default: 5 }}; // in minutes
313
+
314
+ let streamModal;
315
+ let data;
166
316
 
167
- let rooms = {
168
- {%- for r in site.data.program -%}
169
- {%- assign room = site.rooms | where: 'name', r.room | first -%}
170
- {%- if room.live -%}
171
-
172
- {%- assign t = r.talks | first -%}
173
- {%- include partials/get_talk_time.html -%}
174
- {%- assign time_start = talk_start -%}
175
- {%- assign time_end = talk_end -%}
176
- {%- include partials/get_timestamp.html -%}
177
-
178
- {%- assign offset_start = site.conference.live.streaming.start_early | default: 0 -%}
179
- {%- assign room_ts_start = offset_start | times: -60 | plus: timestamp_start -%}
180
-
181
- {%- assign t = r.talks | last -%}
182
- {%- include partials/get_talk_time.html -%}
183
- {%- assign time_start = talk_start -%}
184
- {%- assign time_end = talk_end -%}
185
- {%- include partials/get_timestamp.html -%}
186
-
187
- {%- assign offset_end = site.conference.live.streaming.end_late | default: 0 -%}
188
- {%- assign room_ts_end = offset_end | times: 60 | plus: timestamp_end -%}
189
-
190
- "{{ room.name }}": {
191
- "id": {{ forloop.index }},
192
- "href": "{{ room.live }}",
193
- "start": {{ room_ts_start }},
194
- "end": {{ room_ts_end }}
195
- },
196
- {%- endif -%}
197
- {%- endfor -%}
317
+ let getRoom = function (roomName) {
318
+ if (roomName in data.rooms) {
319
+ return data.rooms[roomName];
320
+ }
321
+ else {
322
+ return data.rooms[Object.keys(data.rooms)[0]];
323
+ }
198
324
  };
199
325
 
200
- let streamModal;
201
- let streamTimer;
326
+ let getNextTalk = function (roomName) {
327
+ let timeNow = time();
328
+ let talksHere = data.talks[roomName];
329
+
330
+ if (typeof talksHere !== "undefined") {
331
+ if (timeNow < talksHere[talksHere.length-1].end) {
332
+ for (var i = 0; i < talksHere.length; i++) {
333
+ if (timeNow < talksHere[i].end) {
334
+ return talksHere[i];
335
+ }
336
+ }
337
+ }
338
+ }
339
+ return false;
340
+ };
341
+
342
+ let getNextPause = function (roomName) {
343
+ let timeNow = time();
344
+ let talksHere = data.talks[roomName];
345
+
346
+ if (typeof talksHere !== "undefined") {
347
+ if (timeNow < talksHere[talksHere.length-1].end) {
348
+ for (var i = 1; i < talksHere.length; i++) {
349
+ if (timeNow < talksHere[i].start && streamPause*60 <= talksHere[i].start - talksHere[i-1].end) {
350
+ return {
351
+ 'start': talksHere[i-1].end,
352
+ 'end': talksHere[i].start,
353
+ };
354
+ }
355
+ }
356
+ }
357
+ }
358
+ return false;
359
+ };
202
360
 
203
- let preStartStream = function(href, startTime, endTime) {
361
+ let setStreamContent = function (content) {
204
362
  streamModal.find('iframe').attr('src', '');
205
363
  streamModal.find('iframe').addClass('d-none');
206
- streamModal.find('#stream-placeholder > div').text('{{ site.data.lang[site.conference.lang].live.pre_stream | default: "Live stream has not started yet." }}');
364
+ streamModal.find('#stream-placeholder > div').text(content);
207
365
  streamModal.find('#stream-placeholder').addClass('d-flex');
366
+ };
208
367
 
209
- if(typeof streamTimer !== "undefined") {
210
- clearTimeout(streamTimer);
211
- }
212
- streamTimer = setTimeout(activeStream, (startTime - timeNow())/liveTimerCorr*1000, href, endTime);
213
- }
214
-
215
- let activeStream = function(href, endTime) {
368
+ let setStreamSrc = function (href) {
216
369
  streamModal.find('iframe').attr('src', href);
217
370
  streamModal.find('#stream-placeholder').addClass('d-none').removeClass('d-flex');
218
371
  streamModal.find('iframe').removeClass('d-none');
372
+ };
219
373
 
220
- if(typeof streamTimer !== "undefined") {
221
- clearTimeout(streamTimer);
374
+ let setStreamVideo = function (roomName) {
375
+ let timeNow = time();
376
+
377
+ let talksHere = data.talks[roomName];
378
+ let roomStart = talksHere[0].start;
379
+ let roomEnd = talksHere[talksHere.length-1].end;
380
+
381
+ if (typeof streamVideoTimer !== "undefined") {
382
+ clearInterval(streamVideoTimer);
222
383
  }
223
- streamTimer = setTimeout(postEndStream, (endTime - timeNow())/liveTimerCorr*1000);
224
- }
225
384
 
226
- let postEndStream = function() {
227
- streamModal.find('iframe').attr('src', '');
228
- streamModal.find('iframe').addClass('d-none');
229
- streamModal.find('#stream-placeholder > div').text('{{ site.data.lang[site.conference.lang].live.post_stream | default: "Live stream has ended." }}');
230
- streamModal.find('#stream-placeholder').addClass('d-flex');
231
- }
385
+ // Conference not yet started
386
+ if (timeNow < roomStart - streamPrepend*60) {
387
+ setStreamContent('{{ site.data.lang[site.conference.lang].live.pre_stream | default: "Live stream has not started yet." }}');
232
388
 
233
- let setStream = function (roomName) {
234
- if (roomName in rooms) {
235
- room = rooms[roomName];
389
+ if (!freezeTime) {
390
+ streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomStart - streamPrepend*60) * 1000, roomName);
391
+ }
236
392
  }
393
+
394
+ // Conference is over
395
+ else if (timeNow > roomEnd + streamExtend*60) {
396
+ setStreamContent('{{ site.data.lang[site.conference.lang].live.post_stream | default: "Live stream has ended." }}');
397
+
398
+ if (!freezeTime && demo) {
399
+ streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomEnd - streamPrepend*60) * 1000, roomName);
400
+ }
401
+ }
402
+
403
+ // Conference ongoing
237
404
  else {
238
- room = rooms[Object.keys(rooms)[0]];
405
+ let pauseNext = getNextPause(roomName);
406
+
407
+ // Currently stream is paused
408
+ if (pauseNext && timeNow >= pauseNext.start + streamExtend*60 && timeNow <= pauseNext.end - streamPrepend*60) {
409
+ setStreamContent('{{ site.data.lang[site.conference.lang].live.pause_stream | default: "Live stream is currently paused." }}');
410
+
411
+ if (!freezeTime) {
412
+ streamVideoTimer = setTimeout(setStreamVideo, delayStart(pauseNext.end - streamPrepend*60) * 1000, roomName);
413
+ }
414
+ }
415
+ // Currently a talk is active
416
+ else {
417
+ let room = getRoom(roomName);
418
+ setStreamSrc(room.href);
419
+
420
+ if (!freezeTime) {
421
+ if (pauseNext) {
422
+ streamVideoTimer = setTimeout(setStreamVideo, delayStart(pauseNext.start + streamExtend*60) * 1000, roomName);
423
+ }
424
+ else {
425
+ streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomEnd + streamExtend*60) * 1000, roomName);
426
+ }
427
+ }
428
+ }
239
429
  }
430
+ };
240
431
 
241
- streamModal.find('.modal-footer .btn').removeClass('active');
242
- if (timeNow() < room.start) {
243
- preStartStream(room.href, room.start, room.end);
432
+ let setStreamInfo = function (roomName) {
433
+ let timeNow = time();
434
+ let talkNext = getNextTalk(roomName);
435
+
436
+ if (typeof streamInfoTimer !== "undefined") {
437
+ clearInterval(streamInfoTimer);
244
438
  }
245
- else if (timeNow() > room.end) {
246
- postEndStream();
439
+
440
+ if (timeNow >= talkNext.start - streamPause*60) {
441
+ document.getElementById('stream-info').dataset.time = talkNext.start;
442
+ document.getElementById('stream-info-time').dataset.time = talkNext.start;
443
+ updateLive();
444
+
445
+ streamModal.find('#stream-info-color').removeClass(function (index, className) {
446
+ return (className.match(/(^|\s)border-soft-\S+/g) || []).join(' ');
447
+ });
448
+ streamModal.find('#stream-info-color').addClass('border-soft-' + talkNext.color);
449
+
450
+ streamModal.find('#stream-info-talk').text(talkNext.name).attr('href', talkNext.href);
451
+
452
+ let speakerStr = '';
453
+ for (var i = 0; i < talkNext.speakers.length; i++) {
454
+ let speaker = data.speakers[talkNext.speakers[i]];
455
+ if (speaker.href == '') {
456
+ speakerStr += speaker.name +', '
457
+ }
458
+ else {
459
+ speakerStr += '<a class="text-reset" href="'+ speaker.href +'">'+ speaker.name +'</a>, ';
460
+ }
461
+ }
462
+ speakerStr = speakerStr.slice(0, -2);
463
+ streamModal.find('#stream-info-speakers').html(speakerStr);
464
+
465
+ streamModal.find('#stream-info').removeClass('d-none');
466
+
467
+ if (!freezeTime) {
468
+ streamInfoTimer = setTimeout(setStreamInfo, delayStart(talkNext.end) * 1000, roomName);
469
+ }
247
470
  }
248
471
  else {
249
- activeStream(room.href, room.end);
472
+ streamModal.find('#stream-info').addClass('d-none');
473
+
474
+ if (!freezeTime) {
475
+ if (talkNext) {
476
+ streamInfoTimer = setTimeout(setStreamInfo, delayStart(talkNext.start - streamPause*60) * 1000, roomName);
477
+ }
478
+ else if (demo) {
479
+ let talksHere = data.talks[roomName];
480
+ streamInfoTimer = setTimeout(setStreamInfo, delayStart(talksHere[0].start - streamPrepend*60) * 1000, roomName);
481
+ }
482
+ }
250
483
  }
484
+ };
485
+
486
+ let setStream = function (roomName) {
487
+ streamModal.find('.modal-footer .btn').removeClass('active');
488
+ streamModal.find('#stream-select').selectedIndex = -1;
489
+
490
+ // Recover room name in case of empty default
491
+ let room = getRoom(roomName);
492
+ roomName = room.name;
493
+
494
+ setStreamVideo(roomName);
495
+ setStreamInfo(roomName);
496
+
251
497
  streamModal.find('#stream-button' + room.id).addClass('active');
498
+ streamModal.find('#stream-select').selectedIndex = room.id;
499
+ };
500
+
501
+ let updateStream = function () {
502
+ if (streamModal.hasClass('show')) {
503
+ let activeButton = streamModal.find('.modal-footer .btn.active');
504
+ let roomName = activeButton.data('room');
505
+
506
+ if (typeof roomName !== "undefined") {
507
+ setStream(roomName);
508
+ }
509
+ }
510
+ };
511
+
512
+ let stopUpdateStream = function () {
513
+ if (typeof streamVideoTimer !== "undefined") {
514
+ clearInterval(streamVideoTimer);
515
+ }
516
+ if (typeof streamInfoTimer !== "undefined") {
517
+ clearInterval(streamInfoTimer);
518
+ }
252
519
  };
253
520
 
254
521
  let hideModal = function (event) {
255
522
  streamModal.find('iframe').attr('src', '');
256
523
  streamModal.find('.modal-footer .btn').removeClass('active');
524
+ streamModal.find('#stream-select').selectedIndex = -1;
257
525
  };
258
526
 
259
- let startStream = function() {
527
+ let setupStream = function () {
260
528
  streamModal = $('#stream-modal');
261
529
 
530
+ // configure modal opening buttons
262
531
  streamModal.on('show.bs.modal', function (event) {
263
532
  let button = $(event.relatedTarget);
264
533
  let roomName = button.data('room');
@@ -268,27 +537,70 @@ window.conference.live = (function() {
268
537
  hideModal(event);
269
538
  });
270
539
 
540
+ // configure room selection buttons in modal
271
541
  streamModal.find('.modal-footer .btn').on('click', function(event) {
272
542
  event.preventDefault();
273
543
 
274
- let roomName = $(this).data('room')
544
+ let roomName = $(this).data('room');
545
+ setStream(roomName);
546
+ });
547
+
548
+ // configure room selection menu in modal
549
+ streamModal.find('#stream-select').on('change', function(event) {
550
+ event.preventDefault();
551
+
552
+ let roomName = $(this).children('option:selected').text();
275
553
  setStream(roomName);
276
554
  });
555
+
556
+ // load data
557
+ $.getJSON('{{ site.baseurl }}/assets/js/data.json', function(json) {
558
+ data = json;
559
+ });
560
+ };
561
+
562
+ let setup = function () {
563
+ startUpdateLive();
564
+ setupStream();
565
+ };
566
+
567
+ let update = function () {
568
+ updateLive();
569
+ updateStream();
570
+ };
571
+
572
+ let startUpdate = function () {
573
+ startUpdateLive();
574
+ updateStream();
575
+ };
576
+
577
+ let stopUpdate = function () {
578
+ stopUpdateLive();
579
+ stopUpdateStream();
277
580
  };
278
581
 
279
582
  {%- else -%}
280
583
 
281
- let startStream = function() {};
584
+ let setup = function () {
585
+ startUpdateLive();
586
+ };
282
587
 
283
- {%- endif %}
588
+ let update = function () {
589
+ updateLive();
590
+ };
284
591
 
285
- let init = function () {
286
- startUpdate();
287
- startStream();
288
- };
592
+ let startUpdate = function () {
593
+ startUpdateLive();
594
+ };
595
+
596
+ let stopUpdate = function () {
597
+ stopUpdateLive();
598
+ };
599
+
600
+ {%- endif %}
289
601
 
290
602
  return {
291
- init: init,
603
+ init: setup,
292
604
 
293
605
  pauseTime: pauseTime,
294
606
  continueTime: continueTime,
@@ -297,7 +609,7 @@ window.conference.live = (function() {
297
609
  getTime: getTime,
298
610
 
299
611
  toggleDemo: toggleDemo,
300
- demoOn: demoOn,
612
+ demo: demoOn,
301
613
  durDemo: durDemo,
302
614
  durPause: durPause
303
615
  };