| { | { | ||||
| "interval": 8000 | |||||
| "interval": 8000, | |||||
| "disabled": true | |||||
| } | } |
| <!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> |
| 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); | |||||
| } |
| 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]); | |||||
| } |
| 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); | |||||
| } |
| var fs = require("fs"); | var fs = require("fs"); | ||||
| var pathlib = require("path"); | var pathlib = require("path"); | ||||
| var sendfile = require("./sendfile"); | |||||
| var mimetype = require("./mimetype"); | |||||
| var error = require("./error"); | var error = require("./error"); | ||||
| module.exports = Slideshow; | 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 | // 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; | 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) { | self.serveFiles = function(parts, res) { | ||||
| // Redirect to / if /{name} is requested | |||||
| // Serve index if /{name} is reuested | |||||
| if (parts.pathname.replace(/\//g, "") === self.name) { | 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() { | self.indexExists = function() { | ||||
| awaiters.push(res); | 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; | self.getSlides = getSlides; | ||||
| function getSlides() { | function getSlides() { | ||||
| return slides; | return slides; |
| var urllib = require("url"); | var urllib = require("url"); | ||||
| var Slideshow = require("./js/slideshow"); | var Slideshow = require("./js/slideshow"); | ||||
| var fileserver = require("./js/fileserver"); | |||||
| var admin = require("./js/admin"); | |||||
| var error = require("./js/error"); | var error = require("./js/error"); | ||||
| var conf = JSON.parse(fs.readFileSync("conf.json")); | 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); | .replace(/<<transition_time>>/g, conf.transition_time); | ||||
| var slideshow = Slideshow(conf.slides, conf.interval); | var slideshow = Slideshow(conf.slides, conf.interval); | ||||
| function handler(req, res) { | function handler(req, res) { | ||||
| var parts = urllib.parse(req.url); | 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); | 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 | // /init: send initial information about current slide | ||||
| } else if (parts.pathname === "/init") { | |||||
| } else if (pathname === "/init") { | |||||
| res.end(slideshow.getSlideName()); | res.end(slideshow.getSlideName()); | ||||
| // /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 (pathname === "/await") { | |||||
| slideshow.pushAwaiter(res); | 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 { | } else { | ||||
| var served = false; | var served = false; | ||||
| for (var slide of slideshow.getSlides()) { | for (var slide of slideshow.getSlides()) { | ||||
| // If client requests /{slide-name}/* | // 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); | slide.serveFiles(parts, res); | ||||
| served = true; | served = true; | ||||
| break; | break; |
| <!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> |
| 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)); | |||||
| } |
| #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; | |||||
| } |
| (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; | |||||
| })(); |
| <!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> |
| (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)); |
| 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; | |||||
| } |