jekyll-theme-conference 3.0.2 → 3.1.2
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.
- checksums.yaml +4 -4
- data/README.md +44 -4
- data/_includes/js/conference-live.js +114 -32
- data/_includes/js/conference-map.js +1 -1
- data/_includes/js/conference-modal.js +4 -4
- data/_includes/js/conference-program.js +1 -1
- data/_includes/js/syncscroll.js +1 -1
- data/_includes/partials/modal_live.html +1 -1
- data/_includes/partials/navbar.html +12 -1
- data/_includes/partials/show_live_button.html +5 -1
- data/_layouts/data.html +21 -9
- data/_layouts/home.html +7 -6
- data/_layouts/stream-overview.html +42 -0
- data/_sass/conference.scss +14 -6
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6dfd46bcf846b9381cf7a9f0b88a063339e1ba62ca33bfd979c56b2ffb20621
|
|
4
|
+
data.tar.gz: 9d6fb33e2b5d0c1c690af32947d7c93f8ae3003f78b0c179d28ed7e3dfe825c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b8ace8e151d2bac7f2a4f2624f8ab947cbb255c4c40de094e60ebb0205d87a5047bb33de6faea4deff3276ba6e6d0cea9f1bc920c0bacbc7946bcf999a251de
|
|
7
|
+
data.tar.gz: 96ee3c142c692447badd23a59244614d272f34ff86092fbe138465ac4257484de610ea3b2db9718b4fe7fbe7ff8179d7e69969818e1eba947c58cf3b7a7cfbb5
|
data/README.md
CHANGED
|
@@ -18,6 +18,41 @@ The theme was originally created for the yearly Winterkongress conference of the
|
|
|
18
18
|
- [Demo: Winterkongress](https://digitale-gesellschaft.ch/kongress/)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
* [Gem-based Method](#gem-based-method)
|
|
25
|
+
* [Remote Theme Method](#remote-theme-method)
|
|
26
|
+
- [Setup](#setup)
|
|
27
|
+
* [Jump Start](#jump-start)
|
|
28
|
+
* [Automatic Import](#automatic-import)
|
|
29
|
+
* [Automatic Build](#automatic-build)
|
|
30
|
+
- [Configuration](#configuration)
|
|
31
|
+
* [Theme Verification](#theme-verification)
|
|
32
|
+
* [Collection URLs](#collection-urls)
|
|
33
|
+
* [Language](#language)
|
|
34
|
+
* [Navigation Bar](#navigation-bar)
|
|
35
|
+
* [Main Landing Page](#main-landing-page)
|
|
36
|
+
* [Information Boxes](#information-boxes)
|
|
37
|
+
* [Live Indications & Streaming](#live-indications---streaming)
|
|
38
|
+
* [Talk Settings](#talk-settings)
|
|
39
|
+
* [Speaker Settings](#speaker-settings)
|
|
40
|
+
* [Location Settings](#location-settings)
|
|
41
|
+
* [Program Settings](#program-settings)
|
|
42
|
+
- [Content](#content)
|
|
43
|
+
* [Schedule / Program](#schedule---program)
|
|
44
|
+
* [Talks](#talks)
|
|
45
|
+
* [Speakers](#speakers)
|
|
46
|
+
* [Rooms](#rooms)
|
|
47
|
+
* [Links](#links)
|
|
48
|
+
- [Overview Pages](#overview-pages)
|
|
49
|
+
* [Location / Room Overview](#location---room-overview)
|
|
50
|
+
* [Live Stream Overview](#live-stream-overview)
|
|
51
|
+
* [Additional Pages](#additional-pages)
|
|
52
|
+
- [Design](#design)
|
|
53
|
+
- [License](#license)
|
|
54
|
+
|
|
55
|
+
|
|
21
56
|
## Installation
|
|
22
57
|
|
|
23
58
|
There are three ways to install: As a [Gem-based theme](https://jekyllrb.com/docs/themes/#understanding-gem-based-themes), as a [remote theme](https://github.blog/2017-11-29-use-any-theme-with-github-pages/) (GitHub Pages compatible), or by cloning/forking this repository and reference to it directly.
|
|
@@ -130,7 +165,7 @@ There exists a Python file in this repository, `_tools/create_entries.py`, which
|
|
|
130
165
|
|
|
131
166
|
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
167
|
|
|
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.
|
|
168
|
+
- `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. Requires `purgecss.config.js` to be copied to the project's root too.
|
|
134
169
|
- `test.yml`: automatically tries to build the website upon a new pull request. It can thus be used as status check before merging.
|
|
135
170
|
- `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
171
|
|
|
@@ -319,15 +354,16 @@ conference:
|
|
|
319
354
|
|
|
320
355
|
In order to help users navigating the program during the congress, a _Live_ indication can be shown next to talks which are currently taking place. A small JavaScript functions keeps the site automatically up-to-date (without the need to refresh) showing the indication as soon as the talk has started and hiding it once it is over (according to the timetable indicated in the `_data/program.yml` file).
|
|
321
356
|
|
|
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).
|
|
357
|
+
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). Instead of opening the modal an external link can also be used.
|
|
323
358
|
|
|
324
359
|
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
|
|
325
360
|
|
|
326
361
|
- how long a pause between two consecutive talks has to be for the live indication to pause (`time_stop`),
|
|
327
362
|
- optionally if streaming is enabled (`streaming`) with indications
|
|
328
363
|
+ 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`),
|
|
330
|
-
+ how long a pause between two consecutive talks has to be for the stream to pause (`time_pause`),
|
|
364
|
+
+ how many minutes the stream stays active after a talk (`time_extend`),
|
|
365
|
+
+ how long a pause between two consecutive talks has to be for the stream to pause (`time_pause`), and
|
|
366
|
+
+ optionally an external (absolute) link to which the user will be redirected instead of opening the modal (`external`),
|
|
331
367
|
- optionally a demo mode setting, whereby the JavaScript function cycles through the entire program in five minutes for demonstration purposes (`demo: true`, default: `false`).
|
|
332
368
|
|
|
333
369
|
```yaml
|
|
@@ -587,6 +623,10 @@ If you choose a different location for the overview pages you must:
|
|
|
587
623
|
|
|
588
624
|
The `location` layout contains a map container (if not disabled, see section _Location Settings_ above) which can be customized. See the section above for further details.
|
|
589
625
|
|
|
626
|
+
### Live Stream Overview
|
|
627
|
+
|
|
628
|
+
The `stream-overview` layout contains all active streams on a single page (see the section _Live Indications & Streaming_ above).
|
|
629
|
+
|
|
590
630
|
### Additional Pages
|
|
591
631
|
|
|
592
632
|
Additional static pages can easily be added as files and linked to via navigation bar or main landing page (see above on how to).
|
|
@@ -13,6 +13,8 @@ window.conference.live = (function() {
|
|
|
13
13
|
{%- include partials/get_talk_timestamp.html -%}
|
|
14
14
|
{%- assign conf_end = timestamp_end -%}
|
|
15
15
|
|
|
16
|
+
let data;
|
|
17
|
+
|
|
16
18
|
let confStart = {{ conf_start }};
|
|
17
19
|
let confEnd = {{ conf_end }};
|
|
18
20
|
let confDur = confEnd - confStart;
|
|
@@ -32,26 +34,43 @@ window.conference.live = (function() {
|
|
|
32
34
|
let streamVideoTimer;
|
|
33
35
|
let streamInfoTimer;
|
|
34
36
|
|
|
37
|
+
let loadData = function () {
|
|
38
|
+
// Fetch schedule from external file
|
|
39
|
+
$.getJSON('{{ site.baseurl }}/assets/js/data.json', function(json) {
|
|
40
|
+
data = json;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let getData = function () {
|
|
45
|
+
// Return data
|
|
46
|
+
return data;
|
|
47
|
+
};
|
|
48
|
+
|
|
35
49
|
let mod = function (n, m) {
|
|
50
|
+
// Absolute modulo
|
|
36
51
|
return ((n % m) + m) % m;
|
|
37
52
|
};
|
|
38
53
|
|
|
39
54
|
let timeNow = function () {
|
|
55
|
+
// Current timestamp in seconds
|
|
40
56
|
return Math.floor(Date.now() / 1000);
|
|
41
57
|
};
|
|
42
58
|
|
|
43
59
|
let timeCont = function () {
|
|
60
|
+
// Continuous time (respecting previous pauses)
|
|
44
61
|
return timeNow() - timeOffset;
|
|
45
62
|
};
|
|
46
63
|
|
|
47
64
|
let timeCycle = function () {
|
|
65
|
+
// Cyclic timestamp in seconds
|
|
48
66
|
let actTime = timeNow();
|
|
49
|
-
let relTime = mod(actTime, durDemo + 2*durPause) / durDemo;
|
|
67
|
+
let relTime = mod(actTime, durDemo + 2*durPause) / (durDemo + 2*durPause);
|
|
50
68
|
let cycleTime = mod((demoEnd - demoStart) * relTime - timeOffset, (demoEnd - demoStart)) + demoStart;
|
|
51
69
|
return cycleTime;
|
|
52
70
|
};
|
|
53
71
|
|
|
54
72
|
let time = function () {
|
|
73
|
+
// Return app time
|
|
55
74
|
if (freezeTime) {
|
|
56
75
|
return timeFrozen;
|
|
57
76
|
}
|
|
@@ -64,6 +83,7 @@ window.conference.live = (function() {
|
|
|
64
83
|
};
|
|
65
84
|
|
|
66
85
|
let pauseTime = function () {
|
|
86
|
+
// Pause app time
|
|
67
87
|
if (!freezeTime) {
|
|
68
88
|
timeFrozen = time();
|
|
69
89
|
freezeTime = true;
|
|
@@ -73,6 +93,7 @@ window.conference.live = (function() {
|
|
|
73
93
|
};
|
|
74
94
|
|
|
75
95
|
let continueTime = function () {
|
|
96
|
+
// Continue app time
|
|
76
97
|
if (freezeTime) {
|
|
77
98
|
freezeTime = false;
|
|
78
99
|
timeOffset += time() - timeFrozen;
|
|
@@ -80,18 +101,23 @@ window.conference.live = (function() {
|
|
|
80
101
|
}
|
|
81
102
|
};
|
|
82
103
|
|
|
83
|
-
let resetTime = function (
|
|
104
|
+
let resetTime = function () {
|
|
105
|
+
// Reset app time
|
|
84
106
|
timeOffset = 0;
|
|
85
107
|
freezeTime = false;
|
|
86
108
|
|
|
87
109
|
startUpdate();
|
|
88
110
|
};
|
|
89
111
|
|
|
90
|
-
let setTime = function (newTime, newDay
|
|
112
|
+
let setTime = function (newTime, newDay) {
|
|
113
|
+
// Set and pause app time
|
|
91
114
|
pauseTime();
|
|
92
115
|
|
|
93
116
|
let dayIdx;
|
|
94
|
-
if (
|
|
117
|
+
if (arguments.length < 2) {
|
|
118
|
+
dayIdx = 0;
|
|
119
|
+
}
|
|
120
|
+
else if (Number.isInteger(newDay)) {
|
|
95
121
|
dayIdx = newDay-1;
|
|
96
122
|
}
|
|
97
123
|
else if (/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(newDay)) {
|
|
@@ -100,7 +126,7 @@ window.conference.live = (function() {
|
|
|
100
126
|
else {
|
|
101
127
|
dayIdx = data.days.find(o => o.name === newDay);
|
|
102
128
|
}
|
|
103
|
-
newDate = data.days[dayIdx].date;
|
|
129
|
+
let newDate = data.days[dayIdx].date;
|
|
104
130
|
|
|
105
131
|
let d = new Date(newDate);
|
|
106
132
|
newTime = newTime.split(':');
|
|
@@ -111,7 +137,10 @@ window.conference.live = (function() {
|
|
|
111
137
|
update();
|
|
112
138
|
};
|
|
113
139
|
|
|
114
|
-
let getTime = function (
|
|
140
|
+
let getTime = function () {
|
|
141
|
+
// Return app time as string
|
|
142
|
+
let tConvert = time();
|
|
143
|
+
|
|
115
144
|
let d = new Date(tConvert * 1000);
|
|
116
145
|
let dStr = d.toISOString().slice(0,10);
|
|
117
146
|
let h = d.getHours();
|
|
@@ -121,6 +150,7 @@ window.conference.live = (function() {
|
|
|
121
150
|
};
|
|
122
151
|
|
|
123
152
|
let timeUnit = function () {
|
|
153
|
+
// App time refresh rate
|
|
124
154
|
if (demo) {
|
|
125
155
|
return 0.1;
|
|
126
156
|
}
|
|
@@ -130,6 +160,7 @@ window.conference.live = (function() {
|
|
|
130
160
|
};
|
|
131
161
|
|
|
132
162
|
let delayStart = function (startTime) {
|
|
163
|
+
// Seconds until given startTime occurs
|
|
133
164
|
let tNow = time();
|
|
134
165
|
let tUnit = timeUnit();
|
|
135
166
|
|
|
@@ -149,21 +180,25 @@ window.conference.live = (function() {
|
|
|
149
180
|
};
|
|
150
181
|
|
|
151
182
|
let toggleDemo = function () {
|
|
183
|
+
// Toggle app demo mode
|
|
152
184
|
demo = !demo;
|
|
153
185
|
resetTime();
|
|
154
186
|
};
|
|
155
187
|
|
|
156
188
|
let demoOn = function () {
|
|
189
|
+
// Return app demo status
|
|
157
190
|
return demo;
|
|
158
191
|
};
|
|
159
192
|
|
|
160
193
|
let updateLive = function () {
|
|
194
|
+
// Update status all live elements in DOM
|
|
161
195
|
let tNow = time();
|
|
162
196
|
let liveShow = document.getElementsByClassName('live-show');
|
|
163
197
|
let liveHide = document.getElementsByClassName('live-hide');
|
|
164
198
|
let liveTime = document.getElementsByClassName('live-time');
|
|
165
199
|
let livePast = document.getElementsByClassName('live-past');
|
|
166
200
|
|
|
201
|
+
// Show elements for a given period
|
|
167
202
|
for (let i = 0; i < liveShow.length; i++) {
|
|
168
203
|
let tStarts = liveShow[i].dataset.start.split(',');
|
|
169
204
|
let tEnds = liveShow[i].dataset.end.split(',');
|
|
@@ -181,6 +216,7 @@ window.conference.live = (function() {
|
|
|
181
216
|
}
|
|
182
217
|
}
|
|
183
218
|
|
|
219
|
+
// Hide elements for a given period
|
|
184
220
|
for (let i = 0; i < liveHide.length; i++) {
|
|
185
221
|
let tStarts = liveHide[i].dataset.start.split(',');
|
|
186
222
|
let tEnds = liveHide[i].dataset.end.split(',');
|
|
@@ -200,6 +236,7 @@ window.conference.live = (function() {
|
|
|
200
236
|
}
|
|
201
237
|
}
|
|
202
238
|
|
|
239
|
+
// Update duration string for given elements
|
|
203
240
|
for (let i = 0; i < liveTime.length; i++) {
|
|
204
241
|
let t = liveTime[i].dataset.time;
|
|
205
242
|
if (typeof t == "undefined") {
|
|
@@ -259,6 +296,7 @@ window.conference.live = (function() {
|
|
|
259
296
|
liveTime[i].innerHTML = tStr;
|
|
260
297
|
}
|
|
261
298
|
|
|
299
|
+
// Disable elements for a given period
|
|
262
300
|
for (let i = 0; i < livePast.length; i++) {
|
|
263
301
|
let t = livePast[i].dataset.time;
|
|
264
302
|
if (typeof t == "undefined") {
|
|
@@ -278,13 +316,14 @@ window.conference.live = (function() {
|
|
|
278
316
|
}
|
|
279
317
|
}
|
|
280
318
|
|
|
319
|
+
// Cancel timer after program is over
|
|
281
320
|
if (tNow > confEnd && !demo) {
|
|
282
|
-
// Cancel timer after program is over
|
|
283
321
|
stopUpdateLive();
|
|
284
322
|
}
|
|
285
323
|
};
|
|
286
324
|
|
|
287
325
|
let startUpdateLive = function () {
|
|
326
|
+
// Start update timer to update live elements in DOM
|
|
288
327
|
stopUpdateLive();
|
|
289
328
|
updateLive();
|
|
290
329
|
|
|
@@ -301,6 +340,7 @@ window.conference.live = (function() {
|
|
|
301
340
|
};
|
|
302
341
|
|
|
303
342
|
let stopUpdateLive = function () {
|
|
343
|
+
// stopUpdate update timer to update live elements in DOM
|
|
304
344
|
if (typeof liveTimer !== "undefined") {
|
|
305
345
|
clearInterval(liveTimer);
|
|
306
346
|
}
|
|
@@ -312,9 +352,9 @@ window.conference.live = (function() {
|
|
|
312
352
|
let streamExtend = {{ site.conference.live.streaming.time_extend | default: 5 }}; // in minutes
|
|
313
353
|
|
|
314
354
|
let streamModal;
|
|
315
|
-
let data;
|
|
316
355
|
|
|
317
356
|
let getRoom = function (roomName) {
|
|
357
|
+
// Return room object for given room name
|
|
318
358
|
if (roomName in data.rooms) {
|
|
319
359
|
return data.rooms[roomName];
|
|
320
360
|
}
|
|
@@ -323,11 +363,21 @@ window.conference.live = (function() {
|
|
|
323
363
|
}
|
|
324
364
|
};
|
|
325
365
|
|
|
366
|
+
let getTalks = function (roomName) {
|
|
367
|
+
if (roomName in data.talks) {
|
|
368
|
+
return data.talks[roomName];
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
326
375
|
let getNextTalk = function (roomName) {
|
|
376
|
+
// Get talk object for next talk in given room
|
|
327
377
|
let timeNow = time();
|
|
328
|
-
let talksHere =
|
|
378
|
+
let talksHere = getTalks(roomName);
|
|
329
379
|
|
|
330
|
-
if (
|
|
380
|
+
if (talksHere) {
|
|
331
381
|
if (timeNow < talksHere[talksHere.length-1].end) {
|
|
332
382
|
for (var i = 0; i < talksHere.length; i++) {
|
|
333
383
|
if (timeNow < talksHere[i].end) {
|
|
@@ -340,10 +390,11 @@ window.conference.live = (function() {
|
|
|
340
390
|
};
|
|
341
391
|
|
|
342
392
|
let getNextPause = function (roomName) {
|
|
393
|
+
// Get time object for next pause in given room
|
|
343
394
|
let timeNow = time();
|
|
344
|
-
let talksHere =
|
|
395
|
+
let talksHere = getTalks(roomName);
|
|
345
396
|
|
|
346
|
-
if (
|
|
397
|
+
if (talksHere) {
|
|
347
398
|
if (timeNow < talksHere[talksHere.length-1].end) {
|
|
348
399
|
for (var i = 1; i < talksHere.length; i++) {
|
|
349
400
|
if (timeNow < talksHere[i].start && streamPause*60 <= talksHere[i].start - talksHere[i-1].end) {
|
|
@@ -358,25 +409,49 @@ window.conference.live = (function() {
|
|
|
358
409
|
return false;
|
|
359
410
|
};
|
|
360
411
|
|
|
361
|
-
let
|
|
412
|
+
let setStreamIframeContent = function (content) {
|
|
413
|
+
// Set stream modal iframe to show given text
|
|
362
414
|
streamModal.find('iframe').attr('src', '');
|
|
363
415
|
streamModal.find('iframe').addClass('d-none');
|
|
364
416
|
streamModal.find('#stream-placeholder > div').text(content);
|
|
365
417
|
streamModal.find('#stream-placeholder').addClass('d-flex');
|
|
366
418
|
};
|
|
367
419
|
|
|
368
|
-
let
|
|
420
|
+
let setStreamIframeSrc = function (href) {
|
|
421
|
+
// Set stream modal iframe to show given URL
|
|
369
422
|
streamModal.find('iframe').attr('src', href);
|
|
370
423
|
streamModal.find('#stream-placeholder').addClass('d-none').removeClass('d-flex');
|
|
371
424
|
streamModal.find('iframe').removeClass('d-none');
|
|
372
425
|
};
|
|
373
426
|
|
|
374
427
|
let setStreamVideo = function (roomName) {
|
|
428
|
+
// Update stream modal iframe:
|
|
429
|
+
// Show stream with start/pause/end message (for given room) and keep updated
|
|
375
430
|
let timeNow = time();
|
|
376
431
|
|
|
377
|
-
let talksHere =
|
|
378
|
-
let roomStart
|
|
379
|
-
|
|
432
|
+
let talksHere = getTalks(roomName);
|
|
433
|
+
let roomStart, roomEnd;
|
|
434
|
+
if (talksHere) {
|
|
435
|
+
roomStart = talksHere[0].start;
|
|
436
|
+
roomEnd = talksHere[talksHere.length-1].end;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// If no program for given room, take overall first and last talk
|
|
440
|
+
roomStart = 0;
|
|
441
|
+
roomEnd = 0;
|
|
442
|
+
for (let roomNameTalk in data.talks) {
|
|
443
|
+
talksHere = getTalks(roomNameTalk);
|
|
444
|
+
let crntRoomStart = talksHere[0].start;
|
|
445
|
+
let crntRoomEnd = talksHere[talksHere.length-1].end;
|
|
446
|
+
|
|
447
|
+
if (roomStart == 0 || roomStart > crntRoomStart) {
|
|
448
|
+
roomStart = crntRoomStart;
|
|
449
|
+
}
|
|
450
|
+
if (roomEnd == 0 || roomEnd < crntRoomEnd) {
|
|
451
|
+
roomEnd = crntRoomEnd;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
380
455
|
|
|
381
456
|
if (typeof streamVideoTimer !== "undefined") {
|
|
382
457
|
clearInterval(streamVideoTimer);
|
|
@@ -384,7 +459,7 @@ window.conference.live = (function() {
|
|
|
384
459
|
|
|
385
460
|
// Conference not yet started
|
|
386
461
|
if (timeNow < roomStart - streamPrepend*60) {
|
|
387
|
-
|
|
462
|
+
setStreamIframeContent('{{ site.data.lang[site.conference.lang].live.pre_stream | default: "Live stream has not started yet." }}');
|
|
388
463
|
|
|
389
464
|
if (!freezeTime) {
|
|
390
465
|
streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomStart - streamPrepend*60) * 1000, roomName);
|
|
@@ -393,7 +468,7 @@ window.conference.live = (function() {
|
|
|
393
468
|
|
|
394
469
|
// Conference is over
|
|
395
470
|
else if (timeNow > roomEnd + streamExtend*60) {
|
|
396
|
-
|
|
471
|
+
setStreamIframeContent('{{ site.data.lang[site.conference.lang].live.post_stream | default: "Live stream has ended." }}');
|
|
397
472
|
|
|
398
473
|
if (!freezeTime && demo) {
|
|
399
474
|
streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomEnd - streamPrepend*60) * 1000, roomName);
|
|
@@ -406,7 +481,7 @@ window.conference.live = (function() {
|
|
|
406
481
|
|
|
407
482
|
// Currently stream is paused
|
|
408
483
|
if (pauseNext && timeNow >= pauseNext.start + streamExtend*60 && timeNow <= pauseNext.end - streamPrepend*60) {
|
|
409
|
-
|
|
484
|
+
setStreamIframeContent('{{ site.data.lang[site.conference.lang].live.pause_stream | default: "Live stream is currently paused." }}');
|
|
410
485
|
|
|
411
486
|
if (!freezeTime) {
|
|
412
487
|
streamVideoTimer = setTimeout(setStreamVideo, delayStart(pauseNext.end - streamPrepend*60) * 1000, roomName);
|
|
@@ -415,7 +490,7 @@ window.conference.live = (function() {
|
|
|
415
490
|
// Currently a talk is active
|
|
416
491
|
else {
|
|
417
492
|
let room = getRoom(roomName);
|
|
418
|
-
|
|
493
|
+
setStreamIframeSrc(room.href);
|
|
419
494
|
|
|
420
495
|
if (!freezeTime) {
|
|
421
496
|
if (pauseNext) {
|
|
@@ -430,6 +505,8 @@ window.conference.live = (function() {
|
|
|
430
505
|
};
|
|
431
506
|
|
|
432
507
|
let setStreamInfo = function (roomName) {
|
|
508
|
+
// Update stream modal info bar:
|
|
509
|
+
// Show next talk and speaker (for given room) and keep updated
|
|
433
510
|
let timeNow = time();
|
|
434
511
|
let talkNext = getNextTalk(roomName);
|
|
435
512
|
|
|
@@ -437,7 +514,7 @@ window.conference.live = (function() {
|
|
|
437
514
|
clearInterval(streamInfoTimer);
|
|
438
515
|
}
|
|
439
516
|
|
|
440
|
-
if (timeNow >= talkNext.start - streamPause*60) {
|
|
517
|
+
if (talkNext && timeNow >= talkNext.start - streamPause*60) {
|
|
441
518
|
document.getElementById('stream-info').dataset.time = talkNext.start;
|
|
442
519
|
document.getElementById('stream-info-time').dataset.time = talkNext.start;
|
|
443
520
|
updateLive();
|
|
@@ -476,14 +553,17 @@ window.conference.live = (function() {
|
|
|
476
553
|
streamInfoTimer = setTimeout(setStreamInfo, delayStart(talkNext.start - streamPause*60) * 1000, roomName);
|
|
477
554
|
}
|
|
478
555
|
else if (demo) {
|
|
479
|
-
let talksHere =
|
|
480
|
-
|
|
556
|
+
let talksHere = getTalks(roomName);
|
|
557
|
+
if (talksHere) {
|
|
558
|
+
streamInfoTimer = setTimeout(setStreamInfo, delayStart(talksHere[0].start - streamPrepend*60) * 1000, roomName);
|
|
559
|
+
}
|
|
481
560
|
}
|
|
482
561
|
}
|
|
483
562
|
}
|
|
484
563
|
};
|
|
485
564
|
|
|
486
565
|
let setStream = function (roomName) {
|
|
566
|
+
// Update stream modal (iframe and info bar) for given room
|
|
487
567
|
streamModal.find('.modal-footer .btn').removeClass('active');
|
|
488
568
|
streamModal.find('#stream-select').val(0);
|
|
489
569
|
|
|
@@ -499,6 +579,7 @@ window.conference.live = (function() {
|
|
|
499
579
|
};
|
|
500
580
|
|
|
501
581
|
let updateStream = function () {
|
|
582
|
+
// Update stream modal for currently active room button
|
|
502
583
|
if (streamModal.hasClass('show')) {
|
|
503
584
|
let activeButton = streamModal.find('.modal-footer .btn.active');
|
|
504
585
|
let roomName = activeButton.data('room');
|
|
@@ -510,6 +591,7 @@ window.conference.live = (function() {
|
|
|
510
591
|
};
|
|
511
592
|
|
|
512
593
|
let stopUpdateStream = function () {
|
|
594
|
+
// Stop stream modal update timer
|
|
513
595
|
if (typeof streamVideoTimer !== "undefined") {
|
|
514
596
|
clearInterval(streamVideoTimer);
|
|
515
597
|
}
|
|
@@ -518,13 +600,15 @@ window.conference.live = (function() {
|
|
|
518
600
|
}
|
|
519
601
|
};
|
|
520
602
|
|
|
521
|
-
let hideModal = function (
|
|
603
|
+
let hideModal = function () {
|
|
604
|
+
// Close stream modal
|
|
522
605
|
streamModal.find('iframe').attr('src', '');
|
|
523
606
|
streamModal.find('.modal-footer .btn').removeClass('active');
|
|
524
607
|
streamModal.find('#stream-select').selectedIndex = -1;
|
|
525
608
|
};
|
|
526
609
|
|
|
527
610
|
let setupStream = function () {
|
|
611
|
+
// Setup events when modal opens/closes
|
|
528
612
|
streamModal = $('#stream-modal');
|
|
529
613
|
|
|
530
614
|
// configure modal opening buttons
|
|
@@ -533,8 +617,8 @@ window.conference.live = (function() {
|
|
|
533
617
|
let roomName = button.data('room');
|
|
534
618
|
setStream(roomName);
|
|
535
619
|
});
|
|
536
|
-
streamModal.on('hide.bs.modal', function (
|
|
537
|
-
hideModal(
|
|
620
|
+
streamModal.on('hide.bs.modal', function () {
|
|
621
|
+
hideModal();
|
|
538
622
|
});
|
|
539
623
|
|
|
540
624
|
// configure room selection buttons in modal
|
|
@@ -552,14 +636,10 @@ window.conference.live = (function() {
|
|
|
552
636
|
let roomName = $(this).children('option:selected').text();
|
|
553
637
|
setStream(roomName);
|
|
554
638
|
});
|
|
555
|
-
|
|
556
|
-
// load data
|
|
557
|
-
$.getJSON('{{ site.baseurl }}/assets/js/data.json', function(json) {
|
|
558
|
-
data = json;
|
|
559
|
-
});
|
|
560
639
|
};
|
|
561
640
|
|
|
562
641
|
let setup = function () {
|
|
642
|
+
loadData();
|
|
563
643
|
startUpdateLive();
|
|
564
644
|
setupStream();
|
|
565
645
|
};
|
|
@@ -582,6 +662,7 @@ window.conference.live = (function() {
|
|
|
582
662
|
{%- else -%}
|
|
583
663
|
|
|
584
664
|
let setup = function () {
|
|
665
|
+
loadData();
|
|
585
666
|
startUpdateLive();
|
|
586
667
|
};
|
|
587
668
|
|
|
@@ -601,6 +682,7 @@ window.conference.live = (function() {
|
|
|
601
682
|
|
|
602
683
|
return {
|
|
603
684
|
init: setup,
|
|
685
|
+
getData: getData,
|
|
604
686
|
|
|
605
687
|
pauseTime: pauseTime,
|
|
606
688
|
continueTime: continueTime,
|
|
@@ -51,7 +51,7 @@ window.conference.modal = (function () {
|
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
let hide = function (el
|
|
54
|
+
let hide = function (el) {
|
|
55
55
|
let modal = $(el);
|
|
56
56
|
|
|
57
57
|
modal.find('.modal-title h3').text('');
|
|
@@ -61,13 +61,13 @@ window.conference.modal = (function () {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
let init = function() {
|
|
64
|
-
elSel = '#link-modal';
|
|
64
|
+
let elSel = '#link-modal';
|
|
65
65
|
|
|
66
66
|
$(elSel).on('show.bs.modal', function (event) {
|
|
67
67
|
show(this, event);
|
|
68
68
|
});
|
|
69
|
-
$(elSel).on('hide.bs.modal', function (
|
|
70
|
-
hide(this
|
|
69
|
+
$(elSel).on('hide.bs.modal', function () {
|
|
70
|
+
hide(this);
|
|
71
71
|
});
|
|
72
72
|
};
|
|
73
73
|
|
|
@@ -34,7 +34,7 @@ window.conference.program = (function() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Add current selected day as hash to URL while keeping current scrolling position
|
|
37
|
-
$('a[data-toggle="tab"]').on('shown.bs.tab', function (
|
|
37
|
+
$('a[data-toggle="tab"]').on('shown.bs.tab', function () {
|
|
38
38
|
updateHash(this.hash);
|
|
39
39
|
});
|
|
40
40
|
}
|
data/_includes/js/syncscroll.js
CHANGED
|
@@ -29,7 +29,7 @@ function (exports) {
|
|
|
29
29
|
var elems = document.getElementsByClassName('syncscroll');
|
|
30
30
|
var i, j, el, found, name;
|
|
31
31
|
for (name in names) {
|
|
32
|
-
if (
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(names, name)) {
|
|
33
33
|
for (i = 0; i < names[name].length; i++) {
|
|
34
34
|
names[name][i].removeEventListener(
|
|
35
35
|
'scroll', names[name][i].syn, 0
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<div class="modal-header">
|
|
6
6
|
<h5 class="modal-title">
|
|
7
|
-
{{ site.data.lang[site.conference.lang].live.
|
|
7
|
+
{{ site.data.lang[site.conference.lang].live.stream | default: "Live Stream" }}
|
|
8
8
|
</h5>
|
|
9
9
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
10
10
|
<span aria-hidden="true">×</span>
|
|
@@ -41,7 +41,18 @@
|
|
|
41
41
|
|
|
42
42
|
<li class="nav-item live-show d-none" data-start="{{ live_starts }}" data-end="{{ live_ends }}">
|
|
43
43
|
{%- if site.conference.live.streaming %}
|
|
44
|
-
<a class="nav-link" title="
|
|
44
|
+
<a class="nav-link" title="
|
|
45
|
+
{%- if link.name -%}
|
|
46
|
+
{{- link.name -}}
|
|
47
|
+
{%- else -%}
|
|
48
|
+
{{- site.data.lang[site.conference.lang].live.stream | default: "Live Stream" -}}
|
|
49
|
+
{%- endif -%}
|
|
50
|
+
" {% if site.conference.live.streaming.external -%}
|
|
51
|
+
href="{{ site.conference.live.streaming.external }}"
|
|
52
|
+
{%- else -%}
|
|
53
|
+
data-toggle="modal" data-target="#stream-modal" data-room="{{ r.name }}" href="#"
|
|
54
|
+
{%- endif -%}
|
|
55
|
+
>
|
|
45
56
|
{%- else %}
|
|
46
57
|
{%- assign link_styleclass = "nav-link" -%}
|
|
47
58
|
{%- include partials/get_link.html %}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
{%- include partials/get_talk_timestamp.html -%}
|
|
3
3
|
|
|
4
4
|
{%- if site.conference.live.streaming -%}
|
|
5
|
-
<a title="{{ link.name }}"
|
|
5
|
+
<a title="{{ link.name }}" {% if site.conference.live.streaming.external -%}
|
|
6
|
+
href="{{ site.conference.live.streaming.external }}"
|
|
7
|
+
{%- else -%}
|
|
8
|
+
data-toggle="modal" data-target="#stream-modal" data-room="{{ r.name }}" href="#"
|
|
9
|
+
{%- endif -%}
|
|
6
10
|
{%- else -%}
|
|
7
11
|
<span
|
|
8
12
|
{%- endif %} class="live-show live-button badge badge-dark font-weight-normal text-center d-none {{ live_button_styleclass }}" data-start="{{ timestamp_start }}" data-end="{{ timestamp_end }}">
|
data/_layouts/data.html
CHANGED
|
@@ -12,13 +12,19 @@
|
|
|
12
12
|
],
|
|
13
13
|
|
|
14
14
|
"rooms": {
|
|
15
|
+
{%- assign first_room = true -%}
|
|
15
16
|
{%- for room in site.rooms %}
|
|
16
|
-
{%- if room.live
|
|
17
|
+
{%- if room.live -%}
|
|
18
|
+
{%- unless first_room -%}
|
|
19
|
+
,
|
|
20
|
+
{%- else -%}
|
|
21
|
+
{%- assign first_room = false -%}
|
|
22
|
+
{%- endunless %}
|
|
17
23
|
"{{ room.name }}": {
|
|
18
24
|
"id": {{ forloop.index }},
|
|
19
25
|
"name": "{{ room.name }}",
|
|
20
26
|
"href": "{{ room.live }}"
|
|
21
|
-
}
|
|
27
|
+
}
|
|
22
28
|
{%- endif -%}
|
|
23
29
|
{%- endfor %}
|
|
24
30
|
},
|
|
@@ -41,18 +47,24 @@
|
|
|
41
47
|
},
|
|
42
48
|
|
|
43
49
|
"talks": {
|
|
50
|
+
{%- assign first_room = true -%}
|
|
44
51
|
{%- for room in site.rooms -%}
|
|
45
|
-
{%- assign
|
|
52
|
+
{%- assign first_talk = true -%}
|
|
46
53
|
{%- for d in site.data.program.days -%}
|
|
47
54
|
{%- for r in d.rooms -%}
|
|
48
55
|
{%- if room.name == r.name and room.live %}
|
|
49
56
|
|
|
50
|
-
{%-
|
|
57
|
+
{%- if first_talk -%}
|
|
58
|
+
{%- unless first_room -%}
|
|
59
|
+
,
|
|
60
|
+
{%- else -%}
|
|
61
|
+
{%- assign first_room = false -%}
|
|
62
|
+
{%- endunless %}
|
|
51
63
|
"{{ room.name | replace: '"', '\"' }}": [
|
|
52
|
-
{%- assign
|
|
64
|
+
{%- assign first_talk = false %}
|
|
53
65
|
{%- else -%}
|
|
54
66
|
,
|
|
55
|
-
{%-
|
|
67
|
+
{%- endif -%}
|
|
56
68
|
|
|
57
69
|
{%- for t in r.talks %}
|
|
58
70
|
{%- assign talk = site.talks | where: 'name', t.name | first %}
|
|
@@ -82,9 +94,9 @@
|
|
|
82
94
|
{%- endfor -%}
|
|
83
95
|
{%- endfor -%}
|
|
84
96
|
|
|
85
|
-
{%-
|
|
86
|
-
]
|
|
87
|
-
{%-
|
|
97
|
+
{%- unless first_talk %}
|
|
98
|
+
]
|
|
99
|
+
{%- endunless -%}
|
|
88
100
|
{%- endfor %}
|
|
89
101
|
}
|
|
90
102
|
}
|
data/_layouts/home.html
CHANGED
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
{% if site.conference.main.links %}
|
|
25
25
|
<div class="lead d-print-none">
|
|
26
|
+
{% assign btn_default = 'btn btn-outline-primary btn-lg mt-2 ml-1' %}
|
|
26
27
|
{% for link in site.conference.main.links %}
|
|
27
28
|
|
|
28
29
|
{% if link.menu %}
|
|
29
30
|
<div class="dropdown d-inline">
|
|
30
|
-
<a class="
|
|
31
|
+
<a class="{{ btn_default }} dropdown-toggle" href="#" role="button" id="main-dropdown{{ forloop.index0 }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
31
32
|
{{ link.name }}
|
|
32
33
|
</a>
|
|
33
34
|
<div class="dropdown-menu" aria-labelledby="main-dropdown{{ forloop.index0 }}">
|
|
@@ -48,20 +49,20 @@
|
|
|
48
49
|
{%- if link.name -%}
|
|
49
50
|
{%- assign link_name = link.name -%}
|
|
50
51
|
{%- else -%}
|
|
51
|
-
{%- assign link_name = site.data.lang[site.conference.lang].live.
|
|
52
|
+
{%- assign link_name = site.data.lang[site.conference.lang].live.stream | default: "Live Stream" -%}
|
|
52
53
|
{%- endif -%}
|
|
53
54
|
|
|
54
55
|
{%- if link.name_inactive -%}
|
|
55
|
-
<span class="live-hide
|
|
56
|
+
<span class="live-hide {{ btn_default }} disabled" data-start="{{ live_starts }}" data-end="{{ live_ends }}">
|
|
56
57
|
{{ link.name_inactive }}
|
|
57
58
|
</span>
|
|
58
59
|
{%- endif -%}
|
|
59
60
|
|
|
60
61
|
<span class="live-show d-none" data-start="{{ live_starts }}" data-end="{{ live_ends }}">
|
|
61
62
|
{% if site.conference.live.streaming %}
|
|
62
|
-
<a class="
|
|
63
|
+
<a class="{{ btn_default }}" title="{{ link_name }}" data-toggle="modal" data-target="#stream-modal" data-room="" href="#">
|
|
63
64
|
{% else %}
|
|
64
|
-
{% assign link_styleclass = "
|
|
65
|
+
{% assign link_styleclass = "{{ btn_default }}" %}
|
|
65
66
|
{% include partials/get_link.html %}
|
|
66
67
|
{{ link_tag }}
|
|
67
68
|
{% endif %}
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
</span>
|
|
72
73
|
|
|
73
74
|
{% else %}
|
|
74
|
-
{% assign link_styleclass =
|
|
75
|
+
{% assign link_styleclass = btn_default %}
|
|
75
76
|
{% include partials/get_link.html %}
|
|
76
77
|
{{ link_tag }}
|
|
77
78
|
{{ link.name }}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{% include partials/header.html %}
|
|
2
|
+
|
|
3
|
+
<h1 class="display-5 mb-4">
|
|
4
|
+
{% if page.title %}
|
|
5
|
+
{{ page.title }}
|
|
6
|
+
{% else %}
|
|
7
|
+
{{ site.data.lang[site.conference.lang].live.stream | default: "Live Streams" }}
|
|
8
|
+
{% endif %}
|
|
9
|
+
</h1>
|
|
10
|
+
|
|
11
|
+
{{ content }}
|
|
12
|
+
|
|
13
|
+
{%- assign nbr_streams = 0 -%}
|
|
14
|
+
{%- for room in site.rooms -%}
|
|
15
|
+
{%- if room.live -%}
|
|
16
|
+
{%- assign mod_nbr_streams = nbr_streams | modulo: 2 -%}
|
|
17
|
+
{%- if mod_nbr_streams == 0 %}
|
|
18
|
+
<div class="row">
|
|
19
|
+
{%- endif %}
|
|
20
|
+
|
|
21
|
+
<div class="col-md">
|
|
22
|
+
<h3>{{ room.name }}</h3>
|
|
23
|
+
<div class="embed-responsive embed-responsive-16by9">
|
|
24
|
+
<iframe class="embed-responsive-item" src="{{ room.live }}" allowfullscreen></iframe>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{%- if mod_nbr_streams == 1 %}
|
|
29
|
+
</div>
|
|
30
|
+
{%- endif -%}
|
|
31
|
+
|
|
32
|
+
{%- assign nbr_streams = nbr_streams | plus: 1 -%}
|
|
33
|
+
{%- endif -%}
|
|
34
|
+
{%- endfor %}
|
|
35
|
+
|
|
36
|
+
{% assign mod_nbr_streams = nbr_streams | modulo: 2 %}
|
|
37
|
+
{%- if mod_nbr_streams == 1 %}
|
|
38
|
+
<div class="col-md"></div>
|
|
39
|
+
</div>
|
|
40
|
+
{%- endif %}
|
|
41
|
+
|
|
42
|
+
{% include partials/footer.html %}
|
data/_sass/conference.scss
CHANGED
|
@@ -79,6 +79,12 @@ ul.btn-group {
|
|
|
79
79
|
|
|
80
80
|
// General
|
|
81
81
|
|
|
82
|
+
// Keep images in container
|
|
83
|
+
main.container img {
|
|
84
|
+
max-width: 100%;
|
|
85
|
+
height: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
// No bottom margin on info-bar
|
|
83
89
|
.alert > p:last-of-type {
|
|
84
90
|
margin-bottom: 0;
|
|
@@ -135,19 +141,21 @@ ul.btn-group {
|
|
|
135
141
|
|
|
136
142
|
// Column widths
|
|
137
143
|
th, td {
|
|
144
|
+
// Total available container width is 1110px
|
|
145
|
+
|
|
138
146
|
// Width of columns with content (default)
|
|
139
|
-
width:
|
|
140
|
-
min-width:
|
|
147
|
+
width: 245px;
|
|
148
|
+
min-width: 245px;
|
|
141
149
|
|
|
142
150
|
// Spacing between columns
|
|
143
151
|
&.col-space {
|
|
144
|
-
width:
|
|
145
|
-
min-width:
|
|
152
|
+
width: 15px;
|
|
153
|
+
min-width: 15px;
|
|
146
154
|
}
|
|
147
155
|
// Width of first column containing timestamps
|
|
148
156
|
&.col-title {
|
|
149
|
-
width:
|
|
150
|
-
min-width:
|
|
157
|
+
width: 70px;
|
|
158
|
+
min-width: 70px;
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-conference
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lorenz Schmid
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-02-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- _layouts/room.html
|
|
109
109
|
- _layouts/speaker-overview.html
|
|
110
110
|
- _layouts/speaker.html
|
|
111
|
+
- _layouts/stream-overview.html
|
|
111
112
|
- _layouts/talk-overview.html
|
|
112
113
|
- _layouts/talk.html
|
|
113
114
|
- _sass/bootstrap/_alert.scss
|
|
@@ -278,7 +279,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
278
279
|
- !ruby/object:Gem::Version
|
|
279
280
|
version: '0'
|
|
280
281
|
requirements: []
|
|
281
|
-
rubygems_version: 3.
|
|
282
|
+
rubygems_version: 3.2.3
|
|
282
283
|
signing_key:
|
|
283
284
|
specification_version: 4
|
|
284
285
|
summary: Jekyll template for a conference website containing program, speaker, talks
|