| @@ -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+"."); | |||