| # 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. |
| { | { | ||||
| "slides": "exampleSlides", | "slides": "exampleSlides", | ||||
| "transition_time": 1, | |||||
| "interval": 5000, | "interval": 5000, | ||||
| "port": 8080 | "port": 8080 | ||||
| } | } |
| <html> | <html> | ||||
| <head> | <head> | ||||
| <meta charset="utf-8"> | <meta charset="utf-8"> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <title>Slides</title> | <title>Slides</title> | ||||
| <style> | <style> | ||||
| * { | |||||
| -moz-user-select: none; | |||||
| -ms-user-select: none; | |||||
| -webkit-user-select: none; | |||||
| user-select: none; | |||||
| cursor: none; | |||||
| } | |||||
| html, body { | html, body { | ||||
| margin: 0px; | margin: 0px; | ||||
| padding: 0px; | padding: 0px; | ||||
| height: 100%; | height: 100%; | ||||
| overflow: hidden; | overflow: hidden; | ||||
| font-size: 2em; | |||||
| font-family: sans-serif; | |||||
| } | } | ||||
| #_overlay { | #_overlay { | ||||
| z-index: 2; | z-index: 2; | ||||
| will-change: opacity; | |||||
| } | } | ||||
| #_main { | #_main { | ||||
| z-index: 1; | 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 { | ._content { | ||||
| background: white; | background: white; | ||||
| position: absolute; | position: absolute; | ||||
| width: 100%; | width: 100%; | ||||
| height: 100%; | height: 100%; | ||||
| top: 0px; | |||||
| top: 0%; | |||||
| left: 0px; | left: 0px; | ||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| } | } | ||||
| ._content { | |||||
| ._content ._wrapper { | |||||
| display: inline-block; | |||||
| text-align: center; | 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 p { font-size: 2.2em } | ||||
| ._content .fullscreen { | ._content .fullscreen { | ||||
| position: absolute; | position: absolute; | ||||
| width: auto; | |||||
| width: 100%; | |||||
| height: 100%; | height: 100%; | ||||
| top: 0px; | top: 0px; | ||||
| left: 50%; | left: 50%; | ||||
| -webkit-transform: translateX(-50%); | -webkit-transform: translateX(-50%); | ||||
| 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 { | #_overlay { | ||||
| transition: opacity 1s; | |||||
| transition: opacity <<transition_time>>s, transform <<transition_time>>s; | |||||
| opacity: 1; | opacity: 1; | ||||
| } | } | ||||
| #_overlay.hidden { | #_overlay.hidden { | ||||
| opacity: 0; | opacity: 0; | ||||
| transform: scale(1.1); | |||||
| } | } | ||||
| </style> | </style> | ||||
| </head> | </head> | ||||
| <body> | <body> | ||||
| <div id="_main" class="_content"></div> | <div id="_main" class="_content"></div> | ||||
| <div id="_overlay" class="_content"></div> | <div id="_overlay" class="_content"></div> | ||||
| <div id="_msg"></div> | |||||
| <!-- Fetch polyfill --> | |||||
| <script src="/polyfills.js"></script> | |||||
| <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 overlay = () => document.querySelector("#_overlay"); | ||||
| var main = () => document.querySelector("#_main"); | 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 | // Swap the IDs of two elements | ||||
| function swap(elem1, elem2) { | function swap(elem1, elem2) { | ||||
| } | } | ||||
| // Change slides with a transition | // Change slides with a transition | ||||
| function update() { | |||||
| function update(name) { | |||||
| overlay().innerHTML = ""; | overlay().innerHTML = ""; | ||||
| overlay().className = "_content"; | overlay().className = "_content"; | ||||
| swap(main(), overlay()); | swap(main(), overlay()); | ||||
| fetch("/slide") | fetch("/slide") | ||||
| .then(response => response.text()) | .then(response => response.text()) | ||||
| .then(text => { | .then(text => { | ||||
| history.replaceState({}, "", "/"+name+"/"); | |||||
| main().innerHTML = "<div class='_wrapper'>"+text+"</div>"; | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| main().innerHTML = text; | |||||
| overlay().className = "_content hidden"; | overlay().className = "_content hidden"; | ||||
| }, 1000); | }, 1000); | ||||
| }) | }) | ||||
| .catch(err => console.error(err)); | .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 | // 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> | </script> | ||||
| </body> | </body> | ||||
| </html> | </html> |
| /* | |||||
| * 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); |
| var pathlib = require("path"); | var pathlib = require("path"); | ||||
| var urllib = require("url"); | var urllib = require("url"); | ||||
| var index = fs.readFileSync("index.html"); | |||||
| var conf = JSON.parse(fs.readFileSync("conf.json")); | 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) { | function error(res, err) { | ||||
| console.trace(err); | console.trace(err); | ||||
| res.end(err.toString()); | 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 | // The individual slide | ||||
| function Slide(dir) { | function Slide(dir) { | ||||
| var self = {}; | var self = {}; | ||||
| self.dir = dir; | 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 { | try { | ||||
| serve(parts, res); | |||||
| fs.accessSync(pathlib.join(dir, "index.html")); | |||||
| return true; | |||||
| } catch (err) { | } catch (err) { | ||||
| if (err.code && err.code === "ENOENT") | |||||
| res.writeHead(404); | |||||
| error(res, err); | |||||
| return false; | |||||
| } | } | ||||
| } | } | ||||
| var currentSlide = null; | var currentSlide = null; | ||||
| var awaiters = []; | 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) { | self.serve = function(req, res) { | ||||
| var parts = urllib.parse(req.url); | var parts = urllib.parse(req.url); | ||||
| if (parts.pathname === "/") { | if (parts.pathname === "/") { | ||||
| res.end(index); | 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 | // /await: long polling, request won't end before a new slide comes | ||||
| } else if (parts.pathname === "/await") { | } else if (parts.pathname === "/await") { | ||||
| awaiters.push(res); | 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 { | } 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 | // This function starts the slideshow and goes through the slides | ||||
| // one by one. When done, it starts again by calling this function again. | // one by one. When done, it starts again by calling this function again. | ||||
| function init() { | function init() { | ||||
| var slides = fs.readdirSync(dir) | |||||
| slides = fs.readdirSync(dir) | |||||
| .sort() | .sort() | ||||
| .map(file => Slide(pathlib.join(dir, file))); | .map(file => Slide(pathlib.join(dir, file))); | ||||
| var slideIndex = 0; | |||||
| slideIndex = 0; | |||||
| currentSlide = slides[slideIndex]; | 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(); | init(); | ||||
| var slideshow = Slideshow(conf.slides, conf.interval); | 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); | 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+"."); |