@@ -1,2 +1,3 @@ | |||
conf.json | |||
slides | |||
node_modules |
@@ -1,3 +0,0 @@ | |||
<h1> Viktig informasjon om greier </h1> | |||
<p> Et eller annet </p> |
@@ -0,0 +1,3 @@ | |||
# Viktig informasjon om greier | |||
Et eller annet |
@@ -1 +0,0 @@ | |||
<h1> This is the second slide </h1> |
@@ -0,0 +1 @@ | |||
# This is the second slide |
@@ -1 +0,0 @@ | |||
<h1> This is the third slide </h1> |
@@ -0,0 +1 @@ | |||
# This is the third slide |
@@ -1 +0,0 @@ | |||
<img class="fullscreen" src="image.svg"> |
@@ -0,0 +1 @@ | |||
![fullscreen](image.svg) |
@@ -1,6 +1,7 @@ | |||
var querystring = require("querystring"); | |||
var fs = require("fs"); | |||
var pathlib = require("path"); | |||
var formidable = require("formidable"); | |||
var basepath = "/admin/api/"; | |||
@@ -71,10 +72,10 @@ var methods = { | |||
}, | |||
// 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; | |||
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) => { | |||
if (err && err.code === "ENOENT") | |||
return respond(null, { text: "" }); | |||
@@ -86,17 +87,49 @@ var methods = { | |||
}, | |||
// 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; | |||
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 => { | |||
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) { |
@@ -14,6 +14,7 @@ function file(wpath, fpath) { | |||
file("/polyfills.js"); | |||
file("/script.js"); | |||
file("/slide.css"); | |||
file("/slide.js"); | |||
file("/admin/", "/admin/index.html"); | |||
file("/admin/style.css"); | |||
file("/admin/lib.js"); |
@@ -1,11 +1,16 @@ | |||
var fs = require("fs"); | |||
var pathlib = require("path"); | |||
var marked = require("marked"); | |||
var mimetype = require("./mimetype"); | |||
var error = require("./error"); | |||
module.exports = Slideshow; | |||
marked.setOptions({ | |||
sanitize: true | |||
}); | |||
var htmlPre = | |||
"<html>"+ | |||
"<head>"+ | |||
@@ -16,6 +21,7 @@ var htmlPre = | |||
"<div id='_wrapper'>"; | |||
var htmlPost = | |||
"<script src='/slide.js'></script>"+ | |||
"</div>"+ | |||
"</body>"+ | |||
"</html>"; | |||
@@ -30,20 +36,13 @@ function sendFile(path, res) { | |||
.pipe(res); | |||
} | |||
function sendIndex(path, res) { | |||
function sendIndex(html, 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); | |||
}); | |||
res.write(html); | |||
res.end(htmlPost); | |||
} | |||
// The individual slide | |||
@@ -52,18 +51,45 @@ function Slide(dir) { | |||
self.dir = dir; | |||
self.name = pathlib.parse(dir).name; | |||
self.stat = null; | |||
self.html = ""; | |||
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) { | |||
sendFile(pathlib.join(self.dir, name), res); | |||
sendFile(self.html, 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) { | |||
// Redirect from /{name} to /{name}/ | |||
@@ -88,7 +114,7 @@ function Slide(dir) { | |||
self.indexExists = function() { | |||
try { | |||
fs.accessSync(pathlib.join(dir, "index.html")); | |||
fs.accessSync(pathlib.join(dir, "index.md")); | |||
return true; | |||
} catch (err) { | |||
return false; |
@@ -0,0 +1,21 @@ | |||
{ | |||
"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" | |||
} |
@@ -1,9 +1,11 @@ | |||
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]); | |||
}).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(res => cb(res.err, res.obj)); | |||
} | |||
@@ -32,8 +34,10 @@ function elem(tag, props, children) { | |||
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; | |||
} | |||
@@ -63,19 +67,38 @@ function elem(tag, props, children) { | |||
return e; | |||
} | |||
function uploadEl() { | |||
var uploadElId = 0; | |||
function uploadEl(args, cb) { | |||
var id = uploadElId++; | |||
var frame; | |||
var form; | |||
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", { | |||
type: "file", | |||
style: "display: none" | |||
}); | |||
style: "display: none", | |||
name: "file" | |||
}).on("change", () => { | |||
form.submit(); | |||
}), | |||
elem("button", { | |||
innerHTML: "Upload" | |||
innerHTML: "Upload", | |||
type: "button" | |||
}).on("click", () => { | |||
fileEl.click(); | |||
}); | |||
}) | |||
]); | |||
} | |||
@@ -92,7 +115,7 @@ function async(n, cb) { | |||
function debounce(fn, ms) { | |||
if (ms === undefined) | |||
ms = 100; | |||
ms = 300; | |||
var timeout; | |||
return function() { |
@@ -62,6 +62,21 @@ | |||
#root.edit #fileList { | |||
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, | |||
@@ -73,6 +88,7 @@ | |||
} | |||
#root.edit #fileList, | |||
#root.edit #slide > .uploader, | |||
#root.edit #html { | |||
margin-bottom: 24px; | |||
} |
@@ -8,13 +8,10 @@ | |||
} | |||
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) | |||
@@ -73,10 +70,40 @@ | |||
res.files.forEach(f => { | |||
elem("div", { className: "file" }, [ | |||
elem("div", { | |||
elem("span", { | |||
className: "name", | |||
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); | |||
}); | |||
}); | |||
@@ -97,11 +124,15 @@ | |||
fileListEl = elem("div", { id: "fileList" }), | |||
uploadEl({ | |||
slide: slide | |||
}, populateFileList), | |||
htmlTextEl = elem("textarea", { | |||
id: "html" | |||
}).on("keydown", debounce(() => { | |||
console.log(htmlTextEl.value); | |||
api("slide_html_update", { | |||
api("slide_content_update", { | |||
slide: slide, | |||
text: htmlTextEl.value | |||
}, err => { | |||
@@ -129,7 +160,7 @@ | |||
//metaDisabledEl.checked = !!res.disabled; | |||
}); | |||
api("slide_html", { slide: slide }, (err, html) => { | |||
api("slide_content", { slide: slide }, (err, html) => { | |||
if (err) | |||
return error(err); | |||
@@ -150,16 +181,16 @@ | |||
name = "main"; | |||
} | |||
setView(name, args); | |||
args = args || []; | |||
args.splice(0, 0, root); | |||
root.clear(); | |||
root.className = name; | |||
views[name].apply(null, args); | |||
} | |||
route(); | |||
window.onpopstate = route; | |||
window.onhashchange = route; | |||
})(); |
@@ -0,0 +1,3 @@ | |||
[].forEach.call(document.getElementsByTagName("img"), function(img) { | |||
img.className = img.alt; | |||
}); |