| @@ -1,3 +1,4 @@ | |||
| { | |||
| "interval": 8000 | |||
| "interval": 8000, | |||
| "disabled": true | |||
| } | |||
| @@ -1,195 +0,0 @@ | |||
| <!DOCTYPE html> | |||
| <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: 0%; | |||
| left: 0px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| ._content ._wrapper { | |||
| display: inline-block; | |||
| text-align: center; | |||
| } | |||
| ._content h1 { font-size: 2em } | |||
| ._content h2 { font-size: 1.4em } | |||
| ._content h3 { font-size: 1.2em } | |||
| ._content p { font-size: 1.4em } | |||
| ._content .fullscreen { | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| top: 0px; | |||
| left: 50%; | |||
| -moz-transform: translateX(-50%); | |||
| -ms-transform: translateX(-50%); | |||
| -webkit-transform: translateX(-50%); | |||
| transform: translateX(-50%); | |||
| } | |||
| ._content img.fullscreen { | |||
| width: auto; | |||
| } | |||
| ._content img.fullscreen.stretch { | |||
| width: 100%; | |||
| } | |||
| ._content ul, | |||
| ._content ol { | |||
| text-align: left; | |||
| line-height: 1.3em; | |||
| } | |||
| #_overlay { | |||
| 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) { | |||
| var tmp = elem1.id; | |||
| elem1.id = elem2.id; | |||
| elem2.id = tmp; | |||
| } | |||
| // Change slides with a transition | |||
| function update(name) { | |||
| overlay().innerHTML = ""; | |||
| overlay().className = "_content"; | |||
| swap(main(), overlay()); | |||
| fetch("/slide") | |||
| .then(response => response.text()) | |||
| .then(text => { | |||
| history.replaceState({}, "", "/"+name+"/"); | |||
| main().innerHTML = "<div class='_wrapper'>"+text+"</div>"; | |||
| setTimeout(() => { | |||
| 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 | |||
| 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(); }); | |||
| } | |||
| await(); | |||
| fetch("/init") | |||
| .then(response => response.text()) | |||
| .then(name => update(name)); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,138 @@ | |||
| var querystring = require("querystring"); | |||
| var fs = require("fs"); | |||
| var pathlib = require("path"); | |||
| var basepath = "/admin/api/"; | |||
| // Used in every method handler to make sure the correct arguments are provided | |||
| function hasargs(query, respond, expected) { | |||
| var missing = []; | |||
| for (var e of expected) { | |||
| if (query[e] === undefined) | |||
| missing.push(e); | |||
| } | |||
| if (missing.length > 0) { | |||
| respond("Missing arguments: "+missing.join(", ")); | |||
| return false; | |||
| } else { | |||
| return true; | |||
| } | |||
| } | |||
| // Method handlers are defined here | |||
| var methods = { | |||
| // List all slides in the slides directory | |||
| list_slides: function(query, conf, req, respond) { | |||
| fs.readdir(conf.slides, (err, files) => { | |||
| if (err) | |||
| return respond(err); | |||
| respond(null, files); | |||
| }); | |||
| }, | |||
| // Get metadata about a slide | |||
| slide_meta: function(query, conf, req, respond) { | |||
| if (!hasargs(query, respond, [ "slide" ])) return; | |||
| var path = pathlib.join(conf.slides, query.slide); | |||
| fs.stat(path, (err, stat) => { | |||
| if (err || !stat.isDirectory()) | |||
| return respond(path+" is not a slide."); | |||
| fs.readFile(pathlib.join(path, "meta.json"), (err, res) => { | |||
| if (err && err.code === "ENOENT") | |||
| return respond(null, {}); | |||
| else if (err) | |||
| return respond(err); | |||
| try { | |||
| respond(null, JSON.parse(res)); | |||
| } catch (err) { | |||
| respond(err); | |||
| } | |||
| }); | |||
| }); | |||
| }, | |||
| // Get a list of files of a slide | |||
| slide_file_list: function(query, conf, req, respond) { | |||
| if (!hasargs(query, respond, [ "slide" ])) return; | |||
| var dir = pathlib.join(conf.slides, query.slide); | |||
| fs.readdir(dir, (err, files) => { | |||
| if (err) | |||
| return respond(err); | |||
| respond(null, { files: files }); | |||
| }); | |||
| }, | |||
| // Get a slide's HTML | |||
| slide_html: function(query, conf, req, respond) { | |||
| if (!hasargs(query, respond, [ "slide" ])) return; | |||
| var path = pathlib.join(conf.slides, query.slide, "index.html"); | |||
| fs.readFile(path, "utf-8", (err, text) => { | |||
| if (err && err.code === "ENOENT") | |||
| return respond(null, { text: "" }); | |||
| else if (err) | |||
| return respond(err); | |||
| respond(null, { text: text }); | |||
| }); | |||
| }, | |||
| // Update a slide's HTML | |||
| slide_html_update: function(query, conf, req, respond) { | |||
| if (!hasargs(query, respond, [ "slide", "text" ])) return; | |||
| var path = pathlib.join(conf.slides, query.slide, "index.html"); | |||
| fs.writeFile(path, query.text, err => { | |||
| if (err) | |||
| respond(err); | |||
| else | |||
| respond(); | |||
| }); | |||
| }, | |||
| } | |||
| exports.canServe = function(parts) { | |||
| // Temporary, while working on stuff | |||
| return false; | |||
| return methods[parts.pathname.replace(basepath, "")] !== undefined; | |||
| } | |||
| exports.serve = function(parts, conf, req, res) { | |||
| var fn = methods[parts.pathname.replace(basepath, "")]; | |||
| if (!fn) { | |||
| res.writeHead(404); | |||
| res.end(); | |||
| return; | |||
| } | |||
| var query = querystring.parse(parts.query); | |||
| for (var i in query) { | |||
| query[i] = decodeURIComponent(query[i]); | |||
| } | |||
| // Better than manually doing res.end(JSON.stringify(obj)) everywhere | |||
| function respond(err, obj) { | |||
| var result = { | |||
| obj: obj, | |||
| err: err ? err.toString() : null | |||
| }; | |||
| if (err) | |||
| res.writeHead(400); | |||
| else | |||
| res.writeHead(200); | |||
| res.end(JSON.stringify(result)); | |||
| } | |||
| // Finally, call method handler | |||
| fn(query, conf, req, respond); | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| var fs = require("fs"); | |||
| var pathlib = require("path"); | |||
| var webroot = "web"; | |||
| var files = {}; | |||
| function file(wpath, fpath) { | |||
| if (fpath === undefined) | |||
| fpath = wpath; | |||
| files[wpath] = fs.readFileSync(pathlib.join(webroot, fpath)); | |||
| } | |||
| file("/polyfills.js"); | |||
| file("/script.js"); | |||
| file("/slide.css"); | |||
| file("/admin/", "/admin/index.html"); | |||
| file("/admin/style.css"); | |||
| file("/admin/lib.js"); | |||
| file("/admin/view.js"); | |||
| exports.canServe = canServe; | |||
| function canServe(parts) { | |||
| return files[parts.pathname] !== undefined; | |||
| } | |||
| exports.serve = serve; | |||
| function serve(parts, res) { | |||
| res.end(files[parts.pathname]); | |||
| } | |||
| @@ -1,15 +0,0 @@ | |||
| var fs = require("fs"); | |||
| var mimetype = require("./mimetype"); | |||
| module.exports = sendfile; | |||
| function sendfile(res, path) { | |||
| res.writeHead(200, { | |||
| "content-type": mimetype(path) | |||
| }); | |||
| fs.createReadStream(path) | |||
| .on("error", err => error(err, res)) | |||
| .pipe(res); | |||
| } | |||
| @@ -1,11 +1,51 @@ | |||
| var fs = require("fs"); | |||
| var pathlib = require("path"); | |||
| var sendfile = require("./sendfile"); | |||
| var mimetype = require("./mimetype"); | |||
| var error = require("./error"); | |||
| module.exports = Slideshow; | |||
| var htmlPre = | |||
| "<html>"+ | |||
| "<head>"+ | |||
| "<link rel='stylesheet' href='/slide.css'>"+ | |||
| "<meta charset='utf-8'>"+ | |||
| "</head>"+ | |||
| "<body>"+ | |||
| "<div id='_wrapper'>"; | |||
| var htmlPost = | |||
| "</div>"+ | |||
| "</body>"+ | |||
| "</html>"; | |||
| function sendFile(path, res) { | |||
| res.writeHead(200, { | |||
| "content-type": mimetype(path) | |||
| }); | |||
| fs.createReadStream(path) | |||
| .on("error", err => error(err, res)) | |||
| .pipe(res); | |||
| } | |||
| function sendIndex(path, res) { | |||
| res.writeHead(200, { | |||
| "content-type": "text/html" | |||
| }); | |||
| res.write(htmlPre); | |||
| fs.readFile(path, (err, text) => { | |||
| if (err) | |||
| text.write(err.toString()); | |||
| else | |||
| res.write(text); | |||
| res.end(htmlPost); | |||
| }); | |||
| } | |||
| // The individual slide | |||
| function Slide(dir) { | |||
| var self = {}; | |||
| @@ -13,20 +53,37 @@ function Slide(dir) { | |||
| self.dir = dir; | |||
| self.name = pathlib.parse(dir).name; | |||
| self.serveSlide = function(parts, res) { | |||
| sendfile(res, pathlib.join(dir, "index.html")); | |||
| self.sendIndex = function(res) { | |||
| sendIndex(pathlib.join(self.dir, "index.html"), res); | |||
| } | |||
| self.sendFile = function(name, res) { | |||
| sendFile(pathlib.join(self.dir, name), res); | |||
| } | |||
| self.serveFiles = function(parts, res) { | |||
| // Redirect to / if /{name} is requested | |||
| // Serve index if /{name} is reuested | |||
| 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)); | |||
| // Redirect from /{name} to /{name}/ | |||
| if (parts.pathname[parts.pathname.length - 1] !== '/') { | |||
| res.writeHead(302, { | |||
| location: parts.pathname+"/" | |||
| }); | |||
| res.end(); | |||
| // Serve index if it's already /{name}/ | |||
| } else { | |||
| self.sendIndex(res); | |||
| } | |||
| } else { | |||
| // Serve other files | |||
| var name = parts.pathname.substring(1).replace(self.name, ""); | |||
| var path = pathlib.join(dir, name); | |||
| sendFile(path, res); | |||
| } | |||
| } | |||
| self.indexExists = function() { | |||
| @@ -91,14 +148,6 @@ function Slideshow(dir, changeInterval) { | |||
| awaiters.push(res); | |||
| } | |||
| self.serveCurrentSlide = serveCurrentSlide; | |||
| function serveCurrentSlide(parts, res) { | |||
| if (currentSlide) | |||
| currentSlide.serveSlide(parts, res); | |||
| else | |||
| error("Current slide requested while no current slide exists", res); | |||
| } | |||
| self.getSlides = getSlides; | |||
| function getSlides() { | |||
| return slides; | |||
| @@ -5,10 +5,12 @@ var pathlib = require("path"); | |||
| var urllib = require("url"); | |||
| var Slideshow = require("./js/slideshow"); | |||
| var fileserver = require("./js/fileserver"); | |||
| var admin = require("./js/admin"); | |||
| var error = require("./js/error"); | |||
| var conf = JSON.parse(fs.readFileSync("conf.json")); | |||
| var index = fs.readFileSync("index.html", "utf-8") | |||
| var index = fs.readFileSync("web/index.html", "utf-8") | |||
| .replace(/<<transition_time>>/g, conf.transition_time); | |||
| var slideshow = Slideshow(conf.slides, conf.interval); | |||
| @@ -26,37 +28,37 @@ process.on("uncaughtException", onexit); | |||
| function handler(req, res) { | |||
| var parts = urllib.parse(req.url); | |||
| var pathname = parts.pathname; | |||
| // /: Send the base site to the client | |||
| if (parts.pathname === "/") { | |||
| // Serve index.html (not part of fileserver because the | |||
| // substitution of <<transition_time>>) | |||
| if (pathname === "/") { | |||
| res.end(index); | |||
| // /polyfills.js: JavaScript polyfills | |||
| } else if (parts.pathname === "polyfills.js") { | |||
| fs.createReadStream("polyfills.js") | |||
| .pipe(res) | |||
| .on("error", err => error(err, res)); | |||
| // Send various files from web/ to the client | |||
| } else if (fileserver.canServe(parts)) { | |||
| fileserver.serve(parts, res); | |||
| // /init: send initial information about current slide | |||
| } else if (parts.pathname === "/init") { | |||
| } else if (pathname === "/init") { | |||
| res.end(slideshow.getSlideName()); | |||
| // /await: long polling, request won't end before a new slide comes | |||
| } else if (parts.pathname === "/await") { | |||
| } else if (pathname === "/await") { | |||
| slideshow.pushAwaiter(res); | |||
| // /slide: serve the current slide's html | |||
| } else if (parts.pathname === "/slide") { | |||
| slideshow.serveCurrentSlide(parts, res); | |||
| // /admin/*: respond to admin queries | |||
| } else if (admin.canServe(parts)) { | |||
| admin.serve(parts, conf, req, res); | |||
| // Serve other files | |||
| // Serve slide files | |||
| } else { | |||
| var served = false; | |||
| for (var slide of slideshow.getSlides()) { | |||
| // If client requests /{slide-name}/* | |||
| if (slide.name === parts.pathname.substr(1, slide.name.length)) { | |||
| if (slide.name === pathname.substr(1, slide.name.length)) { | |||
| slide.serveFiles(parts, res); | |||
| served = true; | |||
| break; | |||
| @@ -0,0 +1,16 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <title>Slides</title> | |||
| <link rel="stylesheet" href="style.css"> | |||
| </head> | |||
| <body> | |||
| <div id="root"></div> | |||
| <script src="/polyfills.js"></script> | |||
| <script src="lib.js"></script> | |||
| <script src="view.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,116 @@ | |||
| function api(method, args, cb) { | |||
| var argstr = "?" + Object.keys(args).map(function(key) { | |||
| return encodeURIComponent(key)+"="+encodeURIComponent(args[key]); | |||
| }).join("&"); | |||
| fetch("/admin/api/"+method+argstr, { method: "POST" }) | |||
| .then(response => response.json()) | |||
| .then(res => cb(res.err, res.obj)); | |||
| } | |||
| function elem(tag, props, children) { | |||
| var e; | |||
| if (tag instanceof HTMLElement) | |||
| e = tag; | |||
| else | |||
| e = document.createElement(tag); | |||
| if (props) { | |||
| for (var i in props) { | |||
| e[i] = props[i]; | |||
| } | |||
| } | |||
| if (children) { | |||
| for (var i in children) { | |||
| e.appendChild(children[i]); | |||
| } | |||
| } | |||
| e.appendTo = function(p) { | |||
| p.appendChild(e); | |||
| return e; | |||
| } | |||
| e.on = function() { | |||
| e.addEventListener.apply(e, arguments); | |||
| return e; | |||
| } | |||
| e.addClass = function(name) { | |||
| if (e.className.indexOf(name) !== -1) | |||
| return e; | |||
| e.className += " "+name; | |||
| return e; | |||
| } | |||
| e.removeClass = function(name) { | |||
| e.className = e.className | |||
| .replace(name, "") | |||
| .trim() | |||
| .replace(/ +/, ""); | |||
| return e; | |||
| } | |||
| e.clear = function() { | |||
| var fc; | |||
| while (fc = e.firstChild) | |||
| e.removeChild(fc); | |||
| return e; | |||
| } | |||
| return e; | |||
| } | |||
| function uploadEl() { | |||
| var fileEl; | |||
| elem("div", { className: "uploader" }, [ | |||
| fileEl = elem("input", { | |||
| type: "file", | |||
| style: "display: none" | |||
| }); | |||
| elem("button", { | |||
| innerHTML: "Upload" | |||
| }).on("click", () => { | |||
| fileEl.click(); | |||
| }); | |||
| ]); | |||
| } | |||
| function async(n, cb) { | |||
| var args = []; | |||
| return function(arg) { | |||
| args.push(arg); | |||
| n -= 1; | |||
| if (n === 0) | |||
| cb(args); | |||
| } | |||
| } | |||
| function debounce(fn, ms) { | |||
| if (ms === undefined) | |||
| ms = 100; | |||
| var timeout; | |||
| return function() { | |||
| if (timeout) | |||
| clearTimeout(timeout); | |||
| timeout = setTimeout(function() { | |||
| fn(); | |||
| timeout = null; | |||
| }, ms); | |||
| } | |||
| } | |||
| function error(msg) { | |||
| alert(msg); | |||
| } | |||
| var $$ = function() { | |||
| return elem(document.querySelector.apply(document, arguments)); | |||
| } | |||
| @@ -0,0 +1,86 @@ | |||
| #root { | |||
| width: 95%; | |||
| max-width: 1000px; | |||
| margin: auto; | |||
| text-align: center; | |||
| } | |||
| #root.main .slide { | |||
| margin: 2%; | |||
| width: 45%; | |||
| min-width: 300px; | |||
| position: relative; | |||
| height: 320px; | |||
| display: inline-block; | |||
| text-align: center; | |||
| } | |||
| #root.main .slide, | |||
| #root.main .slide:visited { | |||
| color: #000; | |||
| text-decoration: none; | |||
| } | |||
| #root.main .slide .preview, | |||
| #root.main .slide .overlay { | |||
| -moz-transform: scale(0.5, 0.5); | |||
| -webkit-transform: scale(0.5, 0.5); | |||
| -o-transform: scale(0.5, 0.5); | |||
| -ms-transform: scale(0.5, 0.5); | |||
| transform: scale(0.5, 0.5); | |||
| -moz-transform-origin: top left; | |||
| -webkit-transform-origin: top left; | |||
| -o-transform-origin: top left; | |||
| -ms-transform-origin: top left; | |||
| transform-origin: top left; | |||
| width: 200%; | |||
| height: 600px; | |||
| border: 2px solid black; | |||
| margin-bottom: -300px; | |||
| margin-right: -400px; | |||
| position: absolute; | |||
| top: 24px; | |||
| left: 0px; | |||
| } | |||
| #root.edit #topBar { | |||
| text-align: center; | |||
| height: 24px; | |||
| margin-bottom: 12px; | |||
| } | |||
| #root.edit #topBar a { | |||
| float: left; | |||
| display: inline-block; | |||
| } | |||
| #root.edit #topBar span { | |||
| display: inline-block; | |||
| margin: auto; | |||
| } | |||
| #root.edit #fileList { | |||
| text-align: left; | |||
| } | |||
| #root.edit #html, | |||
| #root.edit #preview, | |||
| #root.edit #fileList { | |||
| width: 100%; | |||
| border: 1px solid #000; | |||
| box-sizing: border-box; | |||
| } | |||
| #root.edit #fileList, | |||
| #root.edit #html { | |||
| margin-bottom: 24px; | |||
| } | |||
| #root.edit #html { | |||
| height: 200px; | |||
| resize: vertical; | |||
| } | |||
| #root.edit #preview { | |||
| height: 500px; | |||
| } | |||
| @@ -0,0 +1,165 @@ | |||
| (function() { | |||
| var root = $$("#root"); | |||
| var views = { | |||
| main: viewMain, | |||
| edit: viewEdit | |||
| } | |||
| function setView(name, args) { | |||
| args = args || []; | |||
| root.className = name; | |||
| location.hash = name+"/"+args.join("/"); | |||
| } | |||
| function viewMain(root) { | |||
| // Get a list of the slides | |||
| api("list_slides", {}, (err, slides) => { | |||
| if (err) | |||
| return error(msg); | |||
| slides.forEach(function(s) { | |||
| // Create a div for each slide | |||
| var el = elem("a", { | |||
| className: "slide", | |||
| href: "#edit/"+s | |||
| }, [ | |||
| elem("div", { | |||
| className: "name", | |||
| innerText: s | |||
| }), | |||
| elem("iframe", { | |||
| className: "preview", | |||
| src: "/"+s+"/" | |||
| }), | |||
| elem("div", { | |||
| className: "overlay" | |||
| }) | |||
| ]).appendTo(root); | |||
| // Add 'disabled' to the class if it's disabled | |||
| api("slide_meta", { slide: s }, (err, res) => { | |||
| if (err) | |||
| return error(err); | |||
| if (res.disabled) | |||
| el.addClass("disabled"); | |||
| }); | |||
| }); | |||
| }); | |||
| } | |||
| function viewEdit(root, slide) { | |||
| if (!slide) { | |||
| error("No slide provided."); | |||
| setView("main"); | |||
| return; | |||
| } | |||
| var fileListEl; | |||
| var htmlTextEl; | |||
| var previewEl; | |||
| function populateFileList() { | |||
| api("slide_file_list", { slide: slide }, (err, res) => { | |||
| fileListEl.clear(); | |||
| if (err) | |||
| return error(err); | |||
| res.files.forEach(f => { | |||
| elem("div", { className: "file" }, [ | |||
| elem("div", { | |||
| className: "name", | |||
| innerText: f | |||
| }) | |||
| ]).appendTo(fileListEl); | |||
| }); | |||
| }); | |||
| } | |||
| elem("div", { id: "slide" }, [ | |||
| elem("div", { id: "topBar" }, [ | |||
| elem("a", { | |||
| href: "#main", | |||
| innerText: "Back" | |||
| }), | |||
| elem("span", { | |||
| className: "name", | |||
| innerText: slide | |||
| }) | |||
| ]), | |||
| fileListEl = elem("div", { id: "fileList" }), | |||
| htmlTextEl = elem("textarea", { | |||
| id: "html" | |||
| }).on("keydown", debounce(() => { | |||
| console.log(htmlTextEl.value); | |||
| api("slide_html_update", { | |||
| slide: slide, | |||
| text: htmlTextEl.value | |||
| }, err => { | |||
| if (err) | |||
| error(err); | |||
| previewEl.src = previewEl.src; | |||
| }); | |||
| })), | |||
| previewEl = elem("iframe", { | |||
| id: "preview", | |||
| src: "/"+slide+"/" | |||
| }) | |||
| ]).appendTo(root); | |||
| populateFileList(); | |||
| api("slide_meta", { slide: slide }, (err, res) => { | |||
| if (err) { | |||
| error(err); | |||
| setView("main"); | |||
| } | |||
| //metaDisabledEl.checked = !!res.disabled; | |||
| }); | |||
| api("slide_html", { slide: slide }, (err, html) => { | |||
| if (err) | |||
| return error(err); | |||
| htmlTextEl.innerHTML = html.text; | |||
| }); | |||
| } | |||
| function route() { | |||
| var name, args; | |||
| if (location.hash.substring(1)) { | |||
| args = location.hash.substring(1).split("/"); | |||
| name = args[0]; | |||
| args.splice(0, 1); | |||
| if (!views[name]) | |||
| name = "main" | |||
| } else { | |||
| name = "main"; | |||
| } | |||
| setView(name, args); | |||
| args = args || []; | |||
| args.splice(0, 0, root); | |||
| root.clear(); | |||
| views[name].apply(null, args); | |||
| } | |||
| route(); | |||
| window.onpopstate = route; | |||
| })(); | |||
| @@ -0,0 +1,79 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <title>Slides</title> | |||
| <style> | |||
| html, body { | |||
| margin: 0px; | |||
| padding: 0px; | |||
| height: 100%; | |||
| overflow: hidden; | |||
| } | |||
| #overlay, | |||
| #main { | |||
| width: 100%; | |||
| height: 100%; | |||
| position: absolute; | |||
| top: 0px; | |||
| left: 0px; | |||
| cursor: none; | |||
| -moz-user-select: none; | |||
| -ms-user-select: none; | |||
| -webkit-user-select: none; | |||
| user-select: none; | |||
| } | |||
| #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; | |||
| } | |||
| #overlay { | |||
| transition: opacity <<transition_time>>s, transform <<transition_time>>s; | |||
| opacity: 1; | |||
| } | |||
| #overlay.hidden { | |||
| opacity: 0; | |||
| transform: scale(1.1); | |||
| } | |||
| #hideCursor { | |||
| position: absolute; | |||
| top: 0px; | |||
| left: 0px; | |||
| bottom: 0px; | |||
| right: 0px; | |||
| cursor: none; | |||
| z-index: 3; | |||
| } | |||
| </style> | |||
| </head> | |||
| <body> | |||
| <iframe id="main"></iframe> | |||
| <iframe id="overlay"></iframe> | |||
| <div id="msg"></div> | |||
| <div id="hideCursor"></div> | |||
| <script src="/polyfills.js"></script> | |||
| <script src="script.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,71 @@ | |||
| (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) { | |||
| var tmp = elem1.id; | |||
| elem1.id = elem2.id; | |||
| elem2.id = tmp; | |||
| } | |||
| // Change slides with a transition | |||
| function update(name) { | |||
| overlay().className = ""; | |||
| swap(main(), overlay()); | |||
| main().src ="/"+name+"/"; | |||
| main().onload = () => { | |||
| overlay().className = "hidden"; | |||
| } | |||
| } | |||
| 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", { method: "POST" }) | |||
| .then(response => response.json()) | |||
| .then(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(); }); | |||
| } | |||
| await(); | |||
| fetch("/init") | |||
| .then(response => response.text()) | |||
| .then(name => update(name)); | |||
| @@ -0,0 +1,51 @@ | |||
| body { | |||
| background: white; | |||
| margin: 0px; | |||
| padding: 0px; | |||
| width: 100%; | |||
| height: 100%; | |||
| overflow: hidden; | |||
| font-size: 2em; | |||
| font-family: sans-serif; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| text-align: center; | |||
| } | |||
| body #wrapper { | |||
| display: inline-block; | |||
| text-align: center; | |||
| } | |||
| h1 { font-size: 2em } | |||
| h2 { font-size: 1.4em } | |||
| h3 { font-size: 1.2em } | |||
| p { font-size: 1.4em } | |||
| .fullscreen { | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| top: 0px; | |||
| left: 50%; | |||
| -moz-transform: translateX(-50%); | |||
| -ms-transform: translateX(-50%); | |||
| -webkit-transform: translateX(-50%); | |||
| transform: translateX(-50%); | |||
| } | |||
| img.fullscreen { | |||
| width: auto; | |||
| } | |||
| img.fullscreen.stretch { | |||
| width: 100%; | |||
| } | |||
| ul, | |||
| ol { | |||
| text-align: left; | |||
| line-height: 1.3em; | |||
| } | |||