conf.json | conf.json | ||||
slides | slides | ||||
node_modules |
<h1> Viktig informasjon om greier </h1> | |||||
<p> Et eller annet </p> |
# Viktig informasjon om greier | |||||
Et eller annet |
<h1> This is the second slide </h1> |
# This is the second slide |
<h1> This is the third slide </h1> |
# This is the third slide |
<img class="fullscreen" src="image.svg"> |
![fullscreen](image.svg) |
var querystring = require("querystring"); | var querystring = require("querystring"); | ||||
var fs = require("fs"); | var fs = require("fs"); | ||||
var pathlib = require("path"); | var pathlib = require("path"); | ||||
var formidable = require("formidable"); | |||||
var basepath = "/admin/api/"; | var basepath = "/admin/api/"; | ||||
}, | }, | ||||
// Get a slide's HTML | // Get a slide's HTML | ||||
slide_html: function(query, conf, req, respond) { | |||||
slide_content: function(query, conf, req, respond) { | |||||
if (!hasargs(query, respond, [ "slide" ])) return; | if (!hasargs(query, respond, [ "slide" ])) return; | ||||
var path = pathlib.join(conf.slides, query.slide, "index.html"); | |||||
var path = pathlib.join(conf.slides, query.slide, "index.md"); | |||||
fs.readFile(path, "utf-8", (err, text) => { | fs.readFile(path, "utf-8", (err, text) => { | ||||
if (err && err.code === "ENOENT") | if (err && err.code === "ENOENT") | ||||
return respond(null, { text: "" }); | return respond(null, { text: "" }); | ||||
}, | }, | ||||
// Update a slide's HTML | // Update a slide's HTML | ||||
slide_html_update: function(query, conf, req, respond) { | |||||
slide_content_update: function(query, conf, req, respond) { | |||||
if (!hasargs(query, respond, [ "slide", "text" ])) return; | if (!hasargs(query, respond, [ "slide", "text" ])) return; | ||||
var path = pathlib.join(conf.slides, query.slide, "index.html"); | |||||
var path = pathlib.join(conf.slides, query.slide, "index.md"); | |||||
fs.writeFile(path, query.text, err => { | fs.writeFile(path, query.text, err => { | ||||
if (err) | |||||
respond(err); | |||||
else | |||||
respond(); | |||||
respond(err); | |||||
}); | }); | ||||
}, | }, | ||||
// Rename a file | |||||
slide_file_rename: function(query, conf, req, respond) { | |||||
if (!hasargs(query, respond, [ "slide", "from", "to" ])) return; | |||||
var op = pathlib.join(conf.slides, query.slide, query.from); | |||||
var np = pathlib.join(conf.slides, query.slide, query.to); | |||||
fs.rename(op, np, err => respond(err)); | |||||
}, | |||||
// Delete a file | |||||
slide_file_delete: function(query, conf, req, respond) { | |||||
if (!hasargs(query, respond, [ "slide", "file" ])) return; | |||||
var path = pathlib.join(conf.slides, query.slide, query.file); | |||||
fs.unlink(path, err => respond(err)); | |||||
}, | |||||
// Upload a file to a slide | |||||
slide_file_upload: function(query, conf, req, respond) { | |||||
if (!hasargs(query, respond, [ "slide" ])) return; | |||||
var form = new formidable.IncomingForm(); | |||||
form.uploadDir = pathlib.join(conf.slides, query.slide); | |||||
form.keepExtensions = true; | |||||
form.on("fileBegin", (name, file) => { | |||||
file.path = pathlib.join(form.uploadDir, file.name); | |||||
}); | |||||
form.parse(req, (err, fields, files) => { | |||||
respond(); | |||||
}); | |||||
} | |||||
} | } | ||||
exports.canServe = function(parts) { | exports.canServe = function(parts) { |
file("/polyfills.js"); | file("/polyfills.js"); | ||||
file("/script.js"); | file("/script.js"); | ||||
file("/slide.css"); | file("/slide.css"); | ||||
file("/slide.js"); | |||||
file("/admin/", "/admin/index.html"); | file("/admin/", "/admin/index.html"); | ||||
file("/admin/style.css"); | file("/admin/style.css"); | ||||
file("/admin/lib.js"); | file("/admin/lib.js"); |
var fs = require("fs"); | var fs = require("fs"); | ||||
var pathlib = require("path"); | var pathlib = require("path"); | ||||
var marked = require("marked"); | |||||
var mimetype = require("./mimetype"); | var mimetype = require("./mimetype"); | ||||
var error = require("./error"); | var error = require("./error"); | ||||
module.exports = Slideshow; | module.exports = Slideshow; | ||||
marked.setOptions({ | |||||
sanitize: true | |||||
}); | |||||
var htmlPre = | var htmlPre = | ||||
"<html>"+ | "<html>"+ | ||||
"<head>"+ | "<head>"+ | ||||
"<div id='_wrapper'>"; | "<div id='_wrapper'>"; | ||||
var htmlPost = | var htmlPost = | ||||
"<script src='/slide.js'></script>"+ | |||||
"</div>"+ | "</div>"+ | ||||
"</body>"+ | "</body>"+ | ||||
"</html>"; | "</html>"; | ||||
.pipe(res); | .pipe(res); | ||||
} | } | ||||
function sendIndex(path, res) { | |||||
function sendIndex(html, res) { | |||||
res.writeHead(200, { | res.writeHead(200, { | ||||
"content-type": "text/html" | "content-type": "text/html" | ||||
}); | }); | ||||
res.write(htmlPre); | res.write(htmlPre); | ||||
fs.readFile(path, (err, text) => { | |||||
if (err) | |||||
text.write(err.toString()); | |||||
else | |||||
res.write(text); | |||||
res.end(htmlPost); | |||||
}); | |||||
res.write(html); | |||||
res.end(htmlPost); | |||||
} | } | ||||
// The individual slide | // The individual slide | ||||
self.dir = dir; | self.dir = dir; | ||||
self.name = pathlib.parse(dir).name; | self.name = pathlib.parse(dir).name; | ||||
self.stat = null; | |||||
self.html = ""; | |||||
self.sendIndex = function(res) { | self.sendIndex = function(res) { | ||||
sendIndex(pathlib.join(self.dir, "index.html"), res); | |||||
var path = pathlib.join(self.dir, "index.md"); | |||||
fs.stat(pathlib.join(self.dir, "index.md"), (err, stat) => { | |||||
if (err) { | |||||
sendIndex("", res); | |||||
console.log(err); | |||||
return; | |||||
} | |||||
if (!self.stat || !stat || self.stat.mtime !== stat.mtime) { | |||||
fs.readFile(path, "utf-8", (err, md) => { | |||||
if (err) { | |||||
sendIndex(err); | |||||
console.log(err); | |||||
return; | |||||
} | |||||
self.html = marked(md); | |||||
sendIndex(self.html, res); | |||||
}); | |||||
} else { | |||||
sendIndex(self.html, res); | |||||
} | |||||
self.stat = stat; | |||||
}); | |||||
} | } | ||||
self.sendFile = function(name, res) { | self.sendFile = function(name, res) { | ||||
sendFile(pathlib.join(self.dir, name), res); | |||||
sendFile(self.html, res); | |||||
} | } | ||||
self.serveFiles = function(parts, res) { | self.serveFiles = function(parts, res) { | ||||
// Serve index if /{name} is reuested | |||||
// Serve index if /{name} is requested | |||||
if (parts.pathname.replace(/\//g, "") === self.name) { | if (parts.pathname.replace(/\//g, "") === self.name) { | ||||
// Redirect from /{name} to /{name}/ | // Redirect from /{name} to /{name}/ | ||||
self.indexExists = function() { | self.indexExists = function() { | ||||
try { | try { | ||||
fs.accessSync(pathlib.join(dir, "index.html")); | |||||
fs.accessSync(pathlib.join(dir, "index.md")); | |||||
return true; | return true; | ||||
} catch (err) { | } catch (err) { | ||||
return false; | return false; |
{ | |||||
"name": "pipic", | |||||
"version": "1.0.0", | |||||
"description": "Pipic is software for making slideshows. The idea is that you have one server, running a pipic server, and have as many clients as necessary which just display the website hosted by the pipic server.", | |||||
"main": "server.js", | |||||
"dependencies": { | |||||
"formidable": "^1.0.17", | |||||
"marked": "^0.3.6" | |||||
}, | |||||
"devDependencies": {}, | |||||
"scripts": { | |||||
"test": "echo \"Error: no test specified\" && exit 1", | |||||
"start": "node server.js" | |||||
}, | |||||
"repository": { | |||||
"type": "git", | |||||
"url": "http://git.mort.coffee/mort/pipic.git" | |||||
}, | |||||
"author": "Martin Dørum Nygaard <martid0311@gmail.com> (http://mort.coffee)", | |||||
"license": "ISC" | |||||
} |
function api(method, args, cb) { | |||||
var argstr = "?" + Object.keys(args).map(function(key) { | |||||
function argstr(args) { | |||||
return "?" + Object.keys(args).map(function(key) { | |||||
return encodeURIComponent(key)+"="+encodeURIComponent(args[key]); | return encodeURIComponent(key)+"="+encodeURIComponent(args[key]); | ||||
}).join("&"); | }).join("&"); | ||||
} | |||||
fetch("/admin/api/"+method+argstr, { method: "POST" }) | |||||
function api(method, args, cb) { | |||||
fetch("/admin/api/"+method+argstr(args), { method: "POST" }) | |||||
.then(response => response.json()) | .then(response => response.json()) | ||||
.then(res => cb(res.err, res.obj)); | .then(res => cb(res.err, res.obj)); | ||||
} | } | ||||
return e; | return e; | ||||
} | } | ||||
e.on = function() { | |||||
e.addEventListener.apply(e, arguments); | |||||
e.on = function(name, fn) { | |||||
e.addEventListener(name, function(evt) { | |||||
fn.call(e, evt); | |||||
}, false); | |||||
return e; | return e; | ||||
} | } | ||||
return e; | return e; | ||||
} | } | ||||
function uploadEl() { | |||||
var uploadElId = 0; | |||||
function uploadEl(args, cb) { | |||||
var id = uploadElId++; | |||||
var frame; | |||||
var form; | |||||
var fileEl; | var fileEl; | ||||
elem("div", { className: "uploader" }, [ | |||||
return form = elem("form", { | |||||
className: "uploader", | |||||
action: "/admin/api/slide_file_upload"+argstr(args), | |||||
method: "post", | |||||
enctype: "multipart/form-data", | |||||
target: "upload-form-"+id | |||||
}, [ | |||||
frame = elem("iframe", { | |||||
style: "display: none", | |||||
name: "upload-form-"+id | |||||
}).on("load", cb), | |||||
fileEl = elem("input", { | fileEl = elem("input", { | ||||
type: "file", | type: "file", | ||||
style: "display: none" | |||||
}); | |||||
style: "display: none", | |||||
name: "file" | |||||
}).on("change", () => { | |||||
form.submit(); | |||||
}), | |||||
elem("button", { | elem("button", { | ||||
innerHTML: "Upload" | |||||
innerHTML: "Upload", | |||||
type: "button" | |||||
}).on("click", () => { | }).on("click", () => { | ||||
fileEl.click(); | fileEl.click(); | ||||
}); | |||||
}) | |||||
]); | ]); | ||||
} | } | ||||
function debounce(fn, ms) { | function debounce(fn, ms) { | ||||
if (ms === undefined) | if (ms === undefined) | ||||
ms = 100; | |||||
ms = 300; | |||||
var timeout; | var timeout; | ||||
return function() { | return function() { |
#root.edit #fileList { | #root.edit #fileList { | ||||
text-align: left; | text-align: left; | ||||
position: relative; | |||||
} | |||||
#root.edit #fileList .file { | |||||
line-height: 30px; | |||||
border-bottom: 1px solid #ccc; | |||||
} | |||||
#root.edit #fileList .file:last-child { | |||||
border-bottom: none; | |||||
} | |||||
#root.edit #fileList .file .name { | |||||
margin-left: 10px; | |||||
} | |||||
#root.edit #fileList .file .controls { | |||||
position: absolute; | |||||
right: 0px; | |||||
} | } | ||||
#root.edit #html, | #root.edit #html, | ||||
} | } | ||||
#root.edit #fileList, | #root.edit #fileList, | ||||
#root.edit #slide > .uploader, | |||||
#root.edit #html { | #root.edit #html { | ||||
margin-bottom: 24px; | margin-bottom: 24px; | ||||
} | } |
} | } | ||||
function setView(name, args) { | function setView(name, args) { | ||||
args = args || []; | |||||
root.className = name; | |||||
location.hash = name+"/"+args.join("/"); | location.hash = name+"/"+args.join("/"); | ||||
} | } | ||||
function viewMain(root) { | function viewMain(root) { | ||||
// Get a list of the slides | // Get a list of the slides | ||||
api("list_slides", {}, (err, slides) => { | api("list_slides", {}, (err, slides) => { | ||||
if (err) | if (err) | ||||
res.files.forEach(f => { | res.files.forEach(f => { | ||||
elem("div", { className: "file" }, [ | elem("div", { className: "file" }, [ | ||||
elem("div", { | |||||
elem("span", { | |||||
className: "name", | className: "name", | ||||
innerText: f | innerText: f | ||||
}) | |||||
}), | |||||
elem("span", { className: "controls" }, [ | |||||
elem("button", { | |||||
className: "rename", | |||||
innerText: "Rename" | |||||
}).on("click", () => { | |||||
var nwname = prompt("New name?", f); | |||||
if (!nwname) | |||||
return; | |||||
api("slide_file_rename", { | |||||
slide: slide, | |||||
from: f, | |||||
to: nwname | |||||
}, populateFileList); | |||||
}), | |||||
elem("button", { | |||||
className: "delete", | |||||
innerText: "Delete" | |||||
}).on("click", () => { | |||||
if (!confirm("Are you sure you want to delete "+f+"?")) | |||||
return; | |||||
api("slide_file_delete", { | |||||
slide: slide, | |||||
file: f | |||||
}, populateFileList); | |||||
}) | |||||
]) | |||||
]).appendTo(fileListEl); | ]).appendTo(fileListEl); | ||||
}); | }); | ||||
}); | }); | ||||
fileListEl = elem("div", { id: "fileList" }), | fileListEl = elem("div", { id: "fileList" }), | ||||
uploadEl({ | |||||
slide: slide | |||||
}, populateFileList), | |||||
htmlTextEl = elem("textarea", { | htmlTextEl = elem("textarea", { | ||||
id: "html" | id: "html" | ||||
}).on("keydown", debounce(() => { | }).on("keydown", debounce(() => { | ||||
console.log(htmlTextEl.value); | console.log(htmlTextEl.value); | ||||
api("slide_html_update", { | |||||
api("slide_content_update", { | |||||
slide: slide, | slide: slide, | ||||
text: htmlTextEl.value | text: htmlTextEl.value | ||||
}, err => { | }, err => { | ||||
//metaDisabledEl.checked = !!res.disabled; | //metaDisabledEl.checked = !!res.disabled; | ||||
}); | }); | ||||
api("slide_html", { slide: slide }, (err, html) => { | |||||
api("slide_content", { slide: slide }, (err, html) => { | |||||
if (err) | if (err) | ||||
return error(err); | return error(err); | ||||
name = "main"; | name = "main"; | ||||
} | } | ||||
setView(name, args); | |||||
args = args || []; | args = args || []; | ||||
args.splice(0, 0, root); | args.splice(0, 0, root); | ||||
root.clear(); | root.clear(); | ||||
root.className = name; | |||||
views[name].apply(null, args); | views[name].apply(null, args); | ||||
} | } | ||||
route(); | route(); | ||||
window.onpopstate = route; | |||||
window.onhashchange = route; | |||||
})(); | })(); |
[].forEach.call(document.getElementsByTagName("img"), function(img) { | |||||
img.className = img.alt; | |||||
}); |