@@ -0,0 +1,33 @@ | |||
# Pipic | |||
Pipic is software for making slideshows. The idea is that you have one server, | |||
running a pipic server, and have as many clients as necessary which just | |||
display the website hosted by the pipic server. | |||
## Usage | |||
1. Copy `conf.json.example` to `conf.json`, and change the desired preferences. | |||
You may for example want to change `slides` to something other than | |||
'exampleSlides'. | |||
2. Run `node server.js`. | |||
3. Point your clients to the site hosted by the pipic server. | |||
## Automatic fullscreen | |||
There are multiple ways for a client to automatically display the website in | |||
fullscreen. | |||
### Firefox | |||
The easiest way for Firefox is to go to about:config and change | |||
`full-screen-api.allow-trusted-requests-only` to false, and the website will | |||
automatically fullscreen itself. You could also change | |||
`full-screen-api.warning.timeout` to 0 to disable the warning telling you the | |||
website is fullscreen. | |||
### Chrome/Chromium | |||
You could start Chrome/Chromium with the `--start-fullscreen` flag, and the | |||
browser will automatically start in fullscreen mode. For some reason, Chrome | |||
seems to have issues when started from a plain X session without a window | |||
manager though, so I would advise using Firefox. |
@@ -1,5 +1,6 @@ | |||
{ | |||
"slides": "exampleSlides", | |||
"transition_time": 1, | |||
"interval": 5000, | |||
"port": 8080 | |||
} |
@@ -2,44 +2,73 @@ | |||
<html> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<title>Slides</title> | |||
<style> | |||
* { | |||
-moz-user-select: none; | |||
-ms-user-select: none; | |||
-webkit-user-select: none; | |||
user-select: none; | |||
cursor: none; | |||
} | |||
html, body { | |||
margin: 0px; | |||
padding: 0px; | |||
height: 100%; | |||
overflow: hidden; | |||
font-size: 2em; | |||
font-family: sans-serif; | |||
} | |||
#_overlay { | |||
z-index: 2; | |||
will-change: opacity; | |||
} | |||
#_main { | |||
z-index: 1; | |||
} | |||
#_msg { | |||
z-index: 3; | |||
position: absolute; | |||
top: 6px; | |||
right: 6px; | |||
backgrounud: white; | |||
display: none; | |||
font-size: 15px; | |||
} | |||
#_msg.active { | |||
display: block; | |||
} | |||
._content { | |||
background: white; | |||
position: absolute; | |||
width: 100%; | |||
height: 100%; | |||
top: 0px; | |||
top: 0%; | |||
left: 0px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
} | |||
._content { | |||
._content ._wrapper { | |||
display: inline-block; | |||
text-align: center; | |||
} | |||
._content h1 { font-size: 5em } | |||
._content h2 { font-size: 4.5em } | |||
._content h3 { font-size: 4em } | |||
._content h1 { font-size: 2em } | |||
._content h2 { font-size: 1.4em } | |||
._content h3 { font-size: 1.2em } | |||
._content p { font-size: 2.2em } | |||
._content .fullscreen { | |||
position: absolute; | |||
width: auto; | |||
width: 100%; | |||
height: 100%; | |||
top: 0px; | |||
left: 50%; | |||
@@ -48,23 +77,58 @@ | |||
-webkit-transform: translateX(-50%); | |||
transform: translateX(-50%); | |||
} | |||
._content img.fullscreen { | |||
width: auto; | |||
} | |||
._content img.fullscreen.stretch { | |||
width: 100%; | |||
} | |||
._content p, | |||
._content ul, | |||
._content ol { | |||
text-align: left; | |||
line-height: 1.3em; | |||
} | |||
#_overlay { | |||
transition: opacity 1s; | |||
transition: opacity <<transition_time>>s, transform <<transition_time>>s; | |||
opacity: 1; | |||
} | |||
#_overlay.hidden { | |||
opacity: 0; | |||
transform: scale(1.1); | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="_main" class="_content"></div> | |||
<div id="_overlay" class="_content"></div> | |||
<div id="_msg"></div> | |||
<!-- Fetch polyfill --> | |||
<script src="/polyfills.js"></script> | |||
<script> | |||
(function fullscreen() { | |||
var elem = document.body; | |||
var rFS = elem.requestFullScreen || | |||
elem.msRequestFullScreen || | |||
elem.mozRequestFullScreen || | |||
elem.webkitRequestFullScreen; | |||
if (rFS) | |||
rFS.call(elem); | |||
})(); | |||
var overlay = () => document.querySelector("#_overlay"); | |||
var main = () => document.querySelector("#_main"); | |||
var msg = () => document.querySelector("#_msg"); | |||
function message(str) { | |||
msg().innerHTML = str; | |||
msg().className = "active"; | |||
} | |||
// Swap the IDs of two elements | |||
function swap(elem1, elem2) { | |||
@@ -74,7 +138,7 @@ | |||
} | |||
// Change slides with a transition | |||
function update() { | |||
function update(name) { | |||
overlay().innerHTML = ""; | |||
overlay().className = "_content"; | |||
swap(main(), overlay()); | |||
@@ -82,20 +146,51 @@ | |||
fetch("/slide") | |||
.then(response => response.text()) | |||
.then(text => { | |||
history.replaceState({}, "", "/"+name+"/"); | |||
main().innerHTML = "<div class='_wrapper'>"+text+"</div>"; | |||
setTimeout(() => { | |||
main().innerHTML = text; | |||
overlay().className = "_content hidden"; | |||
}, 1000); | |||
}) | |||
.catch(err => console.error(err)); | |||
} | |||
function reload() { | |||
message("Server down, waiting"); | |||
var i = setInterval(() => { | |||
fetch("/") | |||
.then(() => { | |||
history.replaceState({}, "", "/"); | |||
location.reload(); | |||
}) | |||
.catch(() => {}); | |||
}, 1000); | |||
} | |||
function await() { | |||
// Wait for the next slide change, then update again | |||
fetch("/await") | |||
.then(response => update()) | |||
.catch(err => { console.error(err); update(); }); | |||
console.log("fetching"); | |||
fetch("/await", { method: "POST" }) | |||
.then(response => response.json()) | |||
.then(obj => { | |||
console.log("fetched", JSON.stringify(obj)); | |||
if (obj.evt === "next") { | |||
update(obj.args.name); | |||
} else if (obj.evt === "reload") { | |||
return reload(); | |||
} else { | |||
console.log("Unknown event: "+obj.evt); | |||
} | |||
await(); | |||
}) | |||
.catch(err => { console.error(err); await(); }); | |||
} | |||
update(); | |||
await(); | |||
fetch("/init") | |||
.then(response => response.text()) | |||
.then(name => update(name)); | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,438 @@ | |||
/* | |||
* Fetch | |||
* https://github.com/github/fetch | |||
*/ | |||
(function(self) { | |||
'use strict'; | |||
if (self.fetch) { | |||
return | |||
} | |||
var support = { | |||
searchParams: 'URLSearchParams' in self, | |||
iterable: 'Symbol' in self && 'iterator' in Symbol, | |||
blob: 'FileReader' in self && 'Blob' in self && (function() { | |||
try { | |||
new Blob() | |||
return true | |||
} catch(e) { | |||
return false | |||
} | |||
})(), | |||
formData: 'FormData' in self, | |||
arrayBuffer: 'ArrayBuffer' in self | |||
} | |||
function normalizeName(name) { | |||
if (typeof name !== 'string') { | |||
name = String(name) | |||
} | |||
if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { | |||
throw new TypeError('Invalid character in header field name') | |||
} | |||
return name.toLowerCase() | |||
} | |||
function normalizeValue(value) { | |||
if (typeof value !== 'string') { | |||
value = String(value) | |||
} | |||
return value | |||
} | |||
// Build a destructive iterator for the value list | |||
function iteratorFor(items) { | |||
var iterator = { | |||
next: function() { | |||
var value = items.shift() | |||
return {done: value === undefined, value: value} | |||
} | |||
} | |||
if (support.iterable) { | |||
iterator[Symbol.iterator] = function() { | |||
return iterator | |||
} | |||
} | |||
return iterator | |||
} | |||
function Headers(headers) { | |||
this.map = {} | |||
if (headers instanceof Headers) { | |||
headers.forEach(function(value, name) { | |||
this.append(name, value) | |||
}, this) | |||
} else if (headers) { | |||
Object.getOwnPropertyNames(headers).forEach(function(name) { | |||
this.append(name, headers[name]) | |||
}, this) | |||
} | |||
} | |||
Headers.prototype.append = function(name, value) { | |||
name = normalizeName(name) | |||
value = normalizeValue(value) | |||
var list = this.map[name] | |||
if (!list) { | |||
list = [] | |||
this.map[name] = list | |||
} | |||
list.push(value) | |||
} | |||
Headers.prototype['delete'] = function(name) { | |||
delete this.map[normalizeName(name)] | |||
} | |||
Headers.prototype.get = function(name) { | |||
var values = this.map[normalizeName(name)] | |||
return values ? values[0] : null | |||
} | |||
Headers.prototype.getAll = function(name) { | |||
return this.map[normalizeName(name)] || [] | |||
} | |||
Headers.prototype.has = function(name) { | |||
return this.map.hasOwnProperty(normalizeName(name)) | |||
} | |||
Headers.prototype.set = function(name, value) { | |||
this.map[normalizeName(name)] = [normalizeValue(value)] | |||
} | |||
Headers.prototype.forEach = function(callback, thisArg) { | |||
Object.getOwnPropertyNames(this.map).forEach(function(name) { | |||
this.map[name].forEach(function(value) { | |||
callback.call(thisArg, value, name, this) | |||
}, this) | |||
}, this) | |||
} | |||
Headers.prototype.keys = function() { | |||
var items = [] | |||
this.forEach(function(value, name) { items.push(name) }) | |||
return iteratorFor(items) | |||
} | |||
Headers.prototype.values = function() { | |||
var items = [] | |||
this.forEach(function(value) { items.push(value) }) | |||
return iteratorFor(items) | |||
} | |||
Headers.prototype.entries = function() { | |||
var items = [] | |||
this.forEach(function(value, name) { items.push([name, value]) }) | |||
return iteratorFor(items) | |||
} | |||
if (support.iterable) { | |||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries | |||
} | |||
function consumed(body) { | |||
if (body.bodyUsed) { | |||
return Promise.reject(new TypeError('Already read')) | |||
} | |||
body.bodyUsed = true | |||
} | |||
function fileReaderReady(reader) { | |||
return new Promise(function(resolve, reject) { | |||
reader.onload = function() { | |||
resolve(reader.result) | |||
} | |||
reader.onerror = function() { | |||
reject(reader.error) | |||
} | |||
}) | |||
} | |||
function readBlobAsArrayBuffer(blob) { | |||
var reader = new FileReader() | |||
reader.readAsArrayBuffer(blob) | |||
return fileReaderReady(reader) | |||
} | |||
function readBlobAsText(blob) { | |||
var reader = new FileReader() | |||
reader.readAsText(blob) | |||
return fileReaderReady(reader) | |||
} | |||
function Body() { | |||
this.bodyUsed = false | |||
this._initBody = function(body) { | |||
this._bodyInit = body | |||
if (typeof body === 'string') { | |||
this._bodyText = body | |||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) { | |||
this._bodyBlob = body | |||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) { | |||
this._bodyFormData = body | |||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { | |||
this._bodyText = body.toString() | |||
} else if (!body) { | |||
this._bodyText = '' | |||
} else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) { | |||
// Only support ArrayBuffers for POST method. | |||
// Receiving ArrayBuffers happens via Blobs, instead. | |||
} else { | |||
throw new Error('unsupported BodyInit type') | |||
} | |||
if (!this.headers.get('content-type')) { | |||
if (typeof body === 'string') { | |||
this.headers.set('content-type', 'text/plain;charset=UTF-8') | |||
} else if (this._bodyBlob && this._bodyBlob.type) { | |||
this.headers.set('content-type', this._bodyBlob.type) | |||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { | |||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') | |||
} | |||
} | |||
} | |||
if (support.blob) { | |||
this.blob = function() { | |||
var rejected = consumed(this) | |||
if (rejected) { | |||
return rejected | |||
} | |||
if (this._bodyBlob) { | |||
return Promise.resolve(this._bodyBlob) | |||
} else if (this._bodyFormData) { | |||
throw new Error('could not read FormData body as blob') | |||
} else { | |||
return Promise.resolve(new Blob([this._bodyText])) | |||
} | |||
} | |||
this.arrayBuffer = function() { | |||
return this.blob().then(readBlobAsArrayBuffer) | |||
} | |||
this.text = function() { | |||
var rejected = consumed(this) | |||
if (rejected) { | |||
return rejected | |||
} | |||
if (this._bodyBlob) { | |||
return readBlobAsText(this._bodyBlob) | |||
} else if (this._bodyFormData) { | |||
throw new Error('could not read FormData body as text') | |||
} else { | |||
return Promise.resolve(this._bodyText) | |||
} | |||
} | |||
} else { | |||
this.text = function() { | |||
var rejected = consumed(this) | |||
return rejected ? rejected : Promise.resolve(this._bodyText) | |||
} | |||
} | |||
if (support.formData) { | |||
this.formData = function() { | |||
return this.text().then(decode) | |||
} | |||
} | |||
this.json = function() { | |||
return this.text().then(JSON.parse) | |||
} | |||
return this | |||
} | |||
// HTTP methods whose capitalization should be normalized | |||
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] | |||
function normalizeMethod(method) { | |||
var upcased = method.toUpperCase() | |||
return (methods.indexOf(upcased) > -1) ? upcased : method | |||
} | |||
function Request(input, options) { | |||
options = options || {} | |||
var body = options.body | |||
if (Request.prototype.isPrototypeOf(input)) { | |||
if (input.bodyUsed) { | |||
throw new TypeError('Already read') | |||
} | |||
this.url = input.url | |||
this.credentials = input.credentials | |||
if (!options.headers) { | |||
this.headers = new Headers(input.headers) | |||
} | |||
this.method = input.method | |||
this.mode = input.mode | |||
if (!body) { | |||
body = input._bodyInit | |||
input.bodyUsed = true | |||
} | |||
} else { | |||
this.url = input | |||
} | |||
this.credentials = options.credentials || this.credentials || 'omit' | |||
if (options.headers || !this.headers) { | |||
this.headers = new Headers(options.headers) | |||
} | |||
this.method = normalizeMethod(options.method || this.method || 'GET') | |||
this.mode = options.mode || this.mode || null | |||
this.referrer = null | |||
if ((this.method === 'GET' || this.method === 'HEAD') && body) { | |||
throw new TypeError('Body not allowed for GET or HEAD requests') | |||
} | |||
this._initBody(body) | |||
} | |||
Request.prototype.clone = function() { | |||
return new Request(this) | |||
} | |||
function decode(body) { | |||
var form = new FormData() | |||
body.trim().split('&').forEach(function(bytes) { | |||
if (bytes) { | |||
var split = bytes.split('=') | |||
var name = split.shift().replace(/\+/g, ' ') | |||
var value = split.join('=').replace(/\+/g, ' ') | |||
form.append(decodeURIComponent(name), decodeURIComponent(value)) | |||
} | |||
}) | |||
return form | |||
} | |||
function headers(xhr) { | |||
var head = new Headers() | |||
var pairs = (xhr.getAllResponseHeaders() || '').trim().split('\n') | |||
pairs.forEach(function(header) { | |||
var split = header.trim().split(':') | |||
var key = split.shift().trim() | |||
var value = split.join(':').trim() | |||
head.append(key, value) | |||
}) | |||
return head | |||
} | |||
Body.call(Request.prototype) | |||
function Response(bodyInit, options) { | |||
if (!options) { | |||
options = {} | |||
} | |||
this.type = 'default' | |||
this.status = options.status | |||
this.ok = this.status >= 200 && this.status < 300 | |||
this.statusText = options.statusText | |||
this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) | |||
this.url = options.url || '' | |||
this._initBody(bodyInit) | |||
} | |||
Body.call(Response.prototype) | |||
Response.prototype.clone = function() { | |||
return new Response(this._bodyInit, { | |||
status: this.status, | |||
statusText: this.statusText, | |||
headers: new Headers(this.headers), | |||
url: this.url | |||
}) | |||
} | |||
Response.error = function() { | |||
var response = new Response(null, {status: 0, statusText: ''}) | |||
response.type = 'error' | |||
return response | |||
} | |||
var redirectStatuses = [301, 302, 303, 307, 308] | |||
Response.redirect = function(url, status) { | |||
if (redirectStatuses.indexOf(status) === -1) { | |||
throw new RangeError('Invalid status code') | |||
} | |||
return new Response(null, {status: status, headers: {location: url}}) | |||
} | |||
self.Headers = Headers | |||
self.Request = Request | |||
self.Response = Response | |||
self.fetch = function(input, init) { | |||
return new Promise(function(resolve, reject) { | |||
var request | |||
if (Request.prototype.isPrototypeOf(input) && !init) { | |||
request = input | |||
} else { | |||
request = new Request(input, init) | |||
} | |||
var xhr = new XMLHttpRequest() | |||
function responseURL() { | |||
if ('responseURL' in xhr) { | |||
return xhr.responseURL | |||
} | |||
// Avoid security warnings on getResponseHeader when not allowed by CORS | |||
if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { | |||
return xhr.getResponseHeader('X-Request-URL') | |||
} | |||
return | |||
} | |||
xhr.onload = function() { | |||
var options = { | |||
status: xhr.status, | |||
statusText: xhr.statusText, | |||
headers: headers(xhr), | |||
url: responseURL() | |||
} | |||
var body = 'response' in xhr ? xhr.response : xhr.responseText | |||
resolve(new Response(body, options)) | |||
} | |||
xhr.onerror = function() { | |||
reject(new TypeError('Network request failed')) | |||
} | |||
xhr.ontimeout = function() { | |||
reject(new TypeError('Network request failed')) | |||
} | |||
xhr.open(request.method, request.url, true) | |||
if (request.credentials === 'include') { | |||
xhr.withCredentials = true | |||
} | |||
if ('responseType' in xhr && support.blob) { | |||
xhr.responseType = 'blob' | |||
} | |||
request.headers.forEach(function(value, name) { | |||
xhr.setRequestHeader(name, value) | |||
}) | |||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) | |||
}) | |||
} | |||
self.fetch.polyfill = true | |||
})(typeof self !== 'undefined' ? self : this); |
@@ -4,40 +4,91 @@ var crypto = require("crypto"); | |||
var pathlib = require("path"); | |||
var urllib = require("url"); | |||
var index = fs.readFileSync("index.html"); | |||
var conf = JSON.parse(fs.readFileSync("conf.json")); | |||
var index = fs.readFileSync("index.html", "utf-8") | |||
.replace(/<<transition_time>>/g, conf.transition_time); | |||
function error(res, err) { | |||
console.trace(err); | |||
res.end(err.toString()); | |||
} | |||
function mimetype(path) { | |||
var ext = pathlib.extname(path) | |||
.substring(1) | |||
.toLowerCase(); | |||
switch (ext) { | |||
case "html": | |||
case "xml": | |||
return "text/"+ext; | |||
case "png": | |||
case "jpg": | |||
case "jpeg": | |||
case "gif": | |||
return "image/"+ext; | |||
case "svg": | |||
return "image/svg+xml"; | |||
case "mov": | |||
case "mp4": | |||
case "ogv": | |||
case "webm": | |||
return "video/"+ext; | |||
case "mp3": | |||
case "ogg": | |||
case "flac": | |||
case "m4a": | |||
return "audio/"+ext; | |||
default: | |||
return "application/octet-stream"; | |||
} | |||
} | |||
function sendfile(res, path) { | |||
res.writeHead(200, { | |||
"content-type": mimetype(path) | |||
}); | |||
fs.createReadStream(path) | |||
.on("error", err => error(res, err)) | |||
.pipe(res); | |||
} | |||
// The individual slide | |||
function Slide(dir) { | |||
var self = {}; | |||
self.dir = dir; | |||
self.name = pathlib.parse(dir).name; | |||
function serve(parts, res) { | |||
if (parts.pathname === "/slide") { | |||
fs.createReadStream(pathlib.join(dir, "index.html")) | |||
.on("error", err => error(res, err)) | |||
.pipe(res); | |||
} else { | |||
fs.createReadStream(pathlib.join(dir, parts.pathname)) | |||
.on("error", err => error(res, err)) | |||
.pipe(res); | |||
self.serveSlide = function(parts, res) { | |||
sendfile(res, pathlib.join(dir, "index.html")); | |||
} | |||
self.serveFiles = function(parts, res) { | |||
// Redirect to / if /{name} is requested | |||
if (parts.pathname.replace(/\//g, "") === self.name) { | |||
res.writeHead(302, { location: "/" }); | |||
return res.end(); | |||
} | |||
var name = parts.pathname.substring(1).replace(self.name, ""); | |||
sendfile(res, pathlib.join(dir, name)); | |||
} | |||
self.serve = function(parts, res) { | |||
self.indexExists = function() { | |||
try { | |||
serve(parts, res); | |||
fs.accessSync(pathlib.join(dir, "index.html")); | |||
return true; | |||
} catch (err) { | |||
if (err.code && err.code === "ENOENT") | |||
res.writeHead(404); | |||
error(res, err); | |||
return false; | |||
} | |||
} | |||
@@ -51,6 +102,15 @@ function Slideshow(dir, changeInterval) { | |||
var currentSlide = null; | |||
var awaiters = []; | |||
var slides = []; | |||
var slideIndex = 0; | |||
var nextTimeout; | |||
self.sendEvent = function(evt, args) { | |||
var str = JSON.stringify({ evt: evt, args: args }); | |||
awaiters.forEach(res => res.end(str)); | |||
awaiters = []; | |||
} | |||
self.serve = function(req, res) { | |||
var parts = urllib.parse(req.url); | |||
@@ -59,44 +119,81 @@ function Slideshow(dir, changeInterval) { | |||
if (parts.pathname === "/") { | |||
res.end(index); | |||
// /polyfills.js: JavaScript polyfills | |||
} else if (parts.pathname === "/polyfills.js") { | |||
fs.createReadStream("polyfills.js") | |||
.pipe(res) | |||
.on("error", err => res.end(err.toString())); | |||
// /init: send initial information about current slide | |||
} else if (parts.pathname === "/init") { | |||
res.end(currentSlide ? currentSlide.name : ""); | |||
// /await: long polling, request won't end before a new slide comes | |||
} else if (parts.pathname === "/await") { | |||
awaiters.push(res); | |||
// There's a current slide: leave serving files up to the slide | |||
} else if (currentSlide) { | |||
currentSlide.serve(parts, res); | |||
// /slide: serve the current slide's html | |||
} else if (parts.pathname === "/slide" && currentSlide) { | |||
currentSlide.serveSlide(parts, res); | |||
// There's no current slide show | |||
// Serve other files | |||
} else { | |||
res.end("No current slideshow."); | |||
var served = false; | |||
for (var slide of slides) { | |||
// If client requests /{slide-name}/* | |||
if (slide.name === parts.pathname.substr(1, slide.name.length)) { | |||
slide.serveFiles(parts, res); | |||
served = true; | |||
break; | |||
} | |||
} | |||
if (!served) { | |||
res.writeHead(404); | |||
res.end("404"); | |||
} | |||
} | |||
} | |||
function next() { | |||
slideIndex += 1; | |||
// Go to the next slide, or restart | |||
if ((slideIndex >= slides.length)) { | |||
clearTimeout(nextTimeout); | |||
init(); | |||
} else { | |||
currentSlide = slides[slideIndex]; | |||
nextTimeout = setTimeout(next, changeInterval); | |||
} | |||
// End all awaiting connections to notify slide change, | |||
if (currentSlide.indexExists()) { | |||
self.sendEvent("next", { name: currentSlide.name }); | |||
// Or go to the next slide if the current one doesn't have an index.html | |||
} else { | |||
clearTimeout(nextTimeout); | |||
setTimeout(next, 0); | |||
} | |||
} | |||
self.next = next; | |||
// This function starts the slideshow and goes through the slides | |||
// one by one. When done, it starts again by calling this function again. | |||
function init() { | |||
var slides = fs.readdirSync(dir) | |||
slides = fs.readdirSync(dir) | |||
.sort() | |||
.map(file => Slide(pathlib.join(dir, file))); | |||
var slideIndex = 0; | |||
slideIndex = 0; | |||
currentSlide = slides[slideIndex]; | |||
var interval = setInterval(() => { | |||
slideIndex += 1; | |||
// Go to the next slide, or restart | |||
if (slideIndex >= slides.length) { | |||
clearInterval(interval); | |||
init(); | |||
} else { | |||
currentSlide = slides[slideIndex]; | |||
} | |||
// End all awaiting connections to notify slide change | |||
awaiters.forEach(res => res.end()); | |||
}, changeInterval); | |||
nextTimeout = setTimeout(next, changeInterval); | |||
} | |||
init(); | |||
@@ -105,6 +202,23 @@ function Slideshow(dir, changeInterval) { | |||
var slideshow = Slideshow(conf.slides, conf.interval); | |||
http.createServer((req, res) => { | |||
function onexit(code) { | |||
console.log("exiting", code); | |||
slideshow.sendEvent("reload"); | |||
process.exit(); | |||
} | |||
process.on("exit", onexit); | |||
process.on("SIGINT", onexit); | |||
process.on("SIGTERM", onexit); | |||
process.on("uncaughtException", onexit); | |||
var server = http.createServer((req, res) => { | |||
slideshow.serve(req, res); | |||
}).listen(conf.port); | |||
}); | |||
server.on("error", err => { | |||
console.error(err); | |||
system.exit(1); | |||
}); | |||
server.listen(conf.port); | |||
console.log("Server running on port "+conf.port+"."); |