Browse Source

started working on admin interface, moved a lot of stuff around

master
mortie 7 years ago
parent
commit
37841c4d98
15 changed files with 837 additions and 243 deletions
  1. 2
    1
      exampleSlides/1/meta.json
  2. 0
    195
      index.html
  3. 138
    0
      js/admin.js
  4. 30
    0
      js/fileserver.js
  5. 0
    15
      js/sendfile.js
  6. 66
    17
      js/slideshow.js
  7. 17
    15
      server.js
  8. 16
    0
      web/admin/index.html
  9. 116
    0
      web/admin/lib.js
  10. 86
    0
      web/admin/style.css
  11. 165
    0
      web/admin/view.js
  12. 79
    0
      web/index.html
  13. 0
    0
      web/polyfills.js
  14. 71
    0
      web/script.js
  15. 51
    0
      web/slide.css

+ 2
- 1
exampleSlides/1/meta.json View File

@@ -1,3 +1,4 @@
{
"interval": 8000
"interval": 8000,
"disabled": true
}

+ 0
- 195
index.html View File

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

+ 138
- 0
js/admin.js View File

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

+ 30
- 0
js/fileserver.js View File

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

+ 0
- 15
js/sendfile.js View File

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

+ 66
- 17
js/slideshow.js View File

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

+ 17
- 15
server.js View File

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

+ 16
- 0
web/admin/index.html View File

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

+ 116
- 0
web/admin/lib.js View File

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

+ 86
- 0
web/admin/style.css View File

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

+ 165
- 0
web/admin/view.js View File

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

})();

+ 79
- 0
web/index.html View File

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

polyfills.js → web/polyfills.js View File


+ 71
- 0
web/script.js View File

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

+ 51
- 0
web/slide.css View File

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

Loading…
Cancel
Save