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