@@ -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; | |||
} |