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