v1.0.0
@ -1,3 +1,12 @@
|
|||||||
# https://git-scm.com/docs/gitignore
|
# https://git-scm.com/docs/gitignore
|
||||||
# https://help.github.com/articles/ignoring-files
|
# https://help.github.com/articles/ignoring-files
|
||||||
# Example .gitignore files: https://github.com/github/gitignore
|
# Example .gitignore files: https://github.com/github/gitignore
|
||||||
|
/storage/
|
||||||
|
/node_modules/
|
||||||
|
/config.json
|
||||||
|
/persist/
|
||||||
|
/errors/
|
||||||
|
/TODO
|
||||||
|
/thumbnails/
|
||||||
|
/logs/
|
||||||
|
.sass-cache
|
||||||
|
@ -0,0 +1,263 @@
|
|||||||
|
express = require('express')
|
||||||
|
uuid = require "uuid"
|
||||||
|
fs = require "fs"
|
||||||
|
domain = require "domain"
|
||||||
|
|
||||||
|
app = express()
|
||||||
|
|
||||||
|
reportError = (err, type = "error", sync = false) ->
|
||||||
|
errorPayload = {}
|
||||||
|
|
||||||
|
Object.getOwnPropertyNames(err).forEach (key) ->
|
||||||
|
errorPayload[key] = err[key]
|
||||||
|
|
||||||
|
filename = "errors/#{type}-#{Date.now()}-#{uuid.v4()}.json"
|
||||||
|
|
||||||
|
if sync
|
||||||
|
fs.writeFileSync filename, JSON.stringify(errorPayload)
|
||||||
|
else
|
||||||
|
fs.writeFile filename, JSON.stringify(errorPayload)
|
||||||
|
|
||||||
|
# To make absolutely sure that we can log and exit on all uncaught errors.
|
||||||
|
if app.get("env") == "production"
|
||||||
|
logAndExit = (err) ->
|
||||||
|
reportError(err, "error", true)
|
||||||
|
process.exit(1)
|
||||||
|
|
||||||
|
rootDomain = domain.create()
|
||||||
|
rootDomain.on "error", logAndExit
|
||||||
|
|
||||||
|
runWrapper = (func) ->
|
||||||
|
rootDomain.run ->
|
||||||
|
try
|
||||||
|
func()
|
||||||
|
catch err
|
||||||
|
logAndExit(err)
|
||||||
|
else
|
||||||
|
# In development, we don't have to catch uncaught errors. Just run the specified function directly.
|
||||||
|
runWrapper = (func) -> func()
|
||||||
|
|
||||||
|
runWrapper ->
|
||||||
|
path = require('path')
|
||||||
|
|
||||||
|
favicon = require('serve-favicon')
|
||||||
|
logger = require('morgan')
|
||||||
|
cookieParser = require('cookie-parser')
|
||||||
|
bodyParser = require('body-parser')
|
||||||
|
fancyshelf = require "fancyshelf"
|
||||||
|
session = require "express-session"
|
||||||
|
csurf = require "csurf"
|
||||||
|
fileStreamRotator = require "file-stream-rotator"
|
||||||
|
domainMiddleware = require("express-domain-middleware")
|
||||||
|
|
||||||
|
errors = require "errors"
|
||||||
|
PrettyError = require "pretty-error"
|
||||||
|
Convert = require "ansi-to-html"
|
||||||
|
marked = require "marked"
|
||||||
|
moment = require "moment"
|
||||||
|
|
||||||
|
persist = require "./lib/persist"
|
||||||
|
useCsrf = require "./lib/use-csrf"
|
||||||
|
templateUtil = require("./lib/template-util")
|
||||||
|
sessionStore = require("./lib/persist-session")(session)
|
||||||
|
|
||||||
|
config = require "./config.json"
|
||||||
|
knexfile = require("./knexfile").development
|
||||||
|
|
||||||
|
ansiHTML = new Convert(escapeXML: true, newline: true)
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
pe = PrettyError.start()
|
||||||
|
pe.skipPackage "bluebird", "coffee-script", "express", "express-promise-router", "jade"
|
||||||
|
pe.skipNodeFiles()
|
||||||
|
|
||||||
|
errors.create
|
||||||
|
name: "UploadError"
|
||||||
|
|
||||||
|
errors.create
|
||||||
|
name: "UploadTooLarge"
|
||||||
|
parents: errors.UploadError
|
||||||
|
status: 413
|
||||||
|
|
||||||
|
errors.create
|
||||||
|
name: "InvalidFiletype"
|
||||||
|
parents: errors.UploadError
|
||||||
|
status: 415
|
||||||
|
|
||||||
|
errors.create
|
||||||
|
name: "NotAuthenticated"
|
||||||
|
status: 403
|
||||||
|
|
||||||
|
errors.create
|
||||||
|
name: "InvalidInput"
|
||||||
|
status: 422
|
||||||
|
|
||||||
|
# Database setup
|
||||||
|
shelf = fancyshelf
|
||||||
|
engine: knexfile.client
|
||||||
|
host: knexfile.connection.host
|
||||||
|
username: knexfile.connection.user
|
||||||
|
password: knexfile.connection.password
|
||||||
|
database: knexfile.connection.database
|
||||||
|
debug: (app.get("env") == "development")
|
||||||
|
|
||||||
|
# Task runner
|
||||||
|
TaskRunner = require("./lib/task-runner")
|
||||||
|
runner = new TaskRunner(app: app, db: shelf, config: config, thumbnailPath: path.join(__dirname, 'thumbnails'))
|
||||||
|
runner.addTask "mirror", require("./tasks/mirror"), maxConcurrency: 5
|
||||||
|
runner.addTask "thumbnail", require("./tasks/thumbnail")
|
||||||
|
runner.run()
|
||||||
|
|
||||||
|
runner.on "taskQueued", (taskType, task) ->
|
||||||
|
persist.increment "task:#{taskType}:queued"
|
||||||
|
|
||||||
|
runner.on "taskStarted", (taskType, task) ->
|
||||||
|
persist.decrement "task:#{taskType}:queued"
|
||||||
|
persist.increment "task:#{taskType}:running"
|
||||||
|
|
||||||
|
runner.on "taskFailed", (taskType, task) ->
|
||||||
|
persist.decrement "task:#{taskType}:running"
|
||||||
|
persist.increment "task:#{taskType}:failed"
|
||||||
|
|
||||||
|
runner.on "taskCompleted", (taskType, task) ->
|
||||||
|
persist.decrement "task:#{taskType}:running"
|
||||||
|
persist.increment "task:#{taskType}:completed"
|
||||||
|
|
||||||
|
if app.get("env") == "development"
|
||||||
|
runner.on "taskFailed", (taskType, task, err) ->
|
||||||
|
console.log err.stack
|
||||||
|
else
|
||||||
|
runner.on "taskFailed", (taskType, task, err) ->
|
||||||
|
reportError err, "taskFailed"
|
||||||
|
|
||||||
|
# Configure Express
|
||||||
|
app.set('views', path.join(__dirname, 'views'))
|
||||||
|
app.set('view engine', 'jade')
|
||||||
|
|
||||||
|
# Middleware
|
||||||
|
if app.get("env") == "development"
|
||||||
|
app.use(logger('dev'))
|
||||||
|
else
|
||||||
|
accessLogStream = fileStreamRotator.getStream frequency: (config.accessLog.frequency ? "24h"), filename: config.accessLog.filename
|
||||||
|
app.use logger (config.accessLog.format ? "combined"), stream: accessLogStream
|
||||||
|
|
||||||
|
# Using /static/ paths to maintain backwards compatibility.
|
||||||
|
app.use("/static/thumbs", express.static(path.join(__dirname, 'thumbnails')))
|
||||||
|
app.use("/static/pdfjs", express.static(path.join(__dirname, 'public/pdfjs')))
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')))
|
||||||
|
#app.use(favicon(__dirname + '/public/favicon.ico'))
|
||||||
|
|
||||||
|
app.use domainMiddleware
|
||||||
|
app.use templateUtil
|
||||||
|
app.use shelf.express
|
||||||
|
|
||||||
|
app.use session
|
||||||
|
store: new sessionStore
|
||||||
|
persist: persist
|
||||||
|
secret: config.session.signingKey
|
||||||
|
resave: true # TODO: Implement `touch` for the session store, and/or switch to a different store.
|
||||||
|
saveUninitialized: false
|
||||||
|
|
||||||
|
# Load models
|
||||||
|
require("./models")(shelf)
|
||||||
|
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }))
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
app.use (req, res, next) ->
|
||||||
|
res.locals.md = marked
|
||||||
|
|
||||||
|
req.appConfig = config
|
||||||
|
req.persist = persist
|
||||||
|
|
||||||
|
jadeExports = [
|
||||||
|
"announcementVisible"
|
||||||
|
"announcementText"
|
||||||
|
"announcementLinkText"
|
||||||
|
"announcementLink"
|
||||||
|
"maintenanceMode"
|
||||||
|
"maintenanceModeText"
|
||||||
|
"donationGoal"
|
||||||
|
"donationTotal"
|
||||||
|
"showNotice"
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in jadeExports
|
||||||
|
res.locals[key] = persist.getItem "var:#{key}"
|
||||||
|
|
||||||
|
res.locals.bitcoinAddress = config.donations.bitcoinAddress
|
||||||
|
res.locals.isAdmin = req.session.isAdmin
|
||||||
|
|
||||||
|
# This is for logging/reporting errors that should not occur, but that are known to not cause any adverse side-effects.
|
||||||
|
# This should NOT be used for errors that could leave the application in an undefined state!
|
||||||
|
req.reportError = reportError
|
||||||
|
|
||||||
|
req.taskRunner = runner
|
||||||
|
|
||||||
|
next()
|
||||||
|
|
||||||
|
app.use "/", require("./routes/index")
|
||||||
|
app.use "/donate", require("./routes/donate")
|
||||||
|
app.use "/d", require("./routes/document")
|
||||||
|
app.use "/upload", require("./routes/upload")
|
||||||
|
app.use "/gallery", require("./routes/gallery")
|
||||||
|
app.use "/blog", require("./routes/blog")
|
||||||
|
app.use "/admin", csurf(), useCsrf, require("./routes/admin")
|
||||||
|
|
||||||
|
# If no routes match, cause a 404
|
||||||
|
app.use (req, res, next) ->
|
||||||
|
next new errors.Http404Error("The requested page was not found.")
|
||||||
|
|
||||||
|
# Error handling middleware
|
||||||
|
app.use "/static/thumbs", (err, req, res, next) ->
|
||||||
|
# TODO: For some reason, Chrome doesn't always display images that are sent with a 404 status code. Let's stick with 200 for now...
|
||||||
|
#res.status 404
|
||||||
|
if err.status == '404'
|
||||||
|
fs.createReadStream path.join(__dirname, "public/images/no-thumbnail.png")
|
||||||
|
.pipe res
|
||||||
|
else
|
||||||
|
next(err)
|
||||||
|
|
||||||
|
app.use (err, req, res, next) ->
|
||||||
|
statusCode = err.status || 500
|
||||||
|
res.status(statusCode)
|
||||||
|
|
||||||
|
# Dump the error to disk if it's a 500, so that the error reporter can deal with it.
|
||||||
|
if app.get("env") != "development" and statusCode == 500
|
||||||
|
reportError(err)
|
||||||
|
|
||||||
|
# Display the error to the user - amount of details depending on whether the application is running in production mode or not.
|
||||||
|
if (app.get('env') == 'development')
|
||||||
|
stack = err
|
||||||
|
else
|
||||||
|
if statusCode == 500
|
||||||
|
errorMessage = "An unknown error occurred."
|
||||||
|
stack = {stack: "An administrator has been notified, and the error will be resolved as soon as possible. Apologies for the inconvenience."}
|
||||||
|
else
|
||||||
|
errorMessage = err.message
|
||||||
|
stack = {stack: err.explanation, subMessage: err.subMessage}
|
||||||
|
|
||||||
|
if req.headers["X-Requested-With"] == "XMLHttpRequest"
|
||||||
|
res.send {
|
||||||
|
message: errorMessage,
|
||||||
|
error: stack
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if err instanceof errors.NotAuthenticated
|
||||||
|
res.redirect "/admin/login"
|
||||||
|
else
|
||||||
|
if app.get("env") == "development"
|
||||||
|
htmlStack = ansiHTML.toHtml(stack.stack)
|
||||||
|
.replace /#555/g, "#b5b5b5"
|
||||||
|
|
||||||
|
# TODO: It seems aborted responses will result in an attempt to send an error page/header - of course this can't succeed, as the headers have already been sent by that time, and an error is thrown.
|
||||||
|
res.render('error', {
|
||||||
|
message: errorMessage,
|
||||||
|
error: stack,
|
||||||
|
statusCode: statusCode,
|
||||||
|
htmlStack: htmlStack
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = app
|
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env coffee
|
||||||
|
|
||||||
|
app = require('../app')
|
||||||
|
debug = require('debug')('pdfy:server')
|
||||||
|
http = require('http')
|
||||||
|
|
||||||
|
normalizePort = (val) ->
|
||||||
|
port = parseInt val, 10
|
||||||
|
|
||||||
|
if isNaN port
|
||||||
|
return val
|
||||||
|
|
||||||
|
if port >= 0
|
||||||
|
return port
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
onError = (error) ->
|
||||||
|
if error.syscall != "listen"
|
||||||
|
throw error
|
||||||
|
|
||||||
|
bind = if typeof port == "string"
|
||||||
|
"Pipe #{port}"
|
||||||
|
else
|
||||||
|
"Port #{port}"
|
||||||
|
|
||||||
|
switch error.code
|
||||||
|
when "EACCES"
|
||||||
|
console.error "#{bind} requires elevated privileges"
|
||||||
|
process.exit 1
|
||||||
|
when "EADDRINUSE"
|
||||||
|
console.error "#{bind} is already in use"
|
||||||
|
process.exit 1
|
||||||
|
else
|
||||||
|
throw error
|
||||||
|
|
||||||
|
|
||||||
|
onListening = ->
|
||||||
|
addr = server.address()
|
||||||
|
|
||||||
|
bind = if typeof port == "string"
|
||||||
|
"pipe #{port}"
|
||||||
|
else
|
||||||
|
"port #{port}"
|
||||||
|
|
||||||
|
debug("Listening on #{bind}")
|
||||||
|
|
||||||
|
|
||||||
|
port = normalizePort(process.env.PORT || '3000')
|
||||||
|
app.set('port', port)
|
||||||
|
|
||||||
|
server = http.createServer(app)
|
||||||
|
|
||||||
|
server.listen(port)
|
||||||
|
server.on('error', onError)
|
||||||
|
server.on('listening', onListening)
|
@ -0,0 +1,51 @@
|
|||||||
|
chokidar = require "chokidar"
|
||||||
|
nodemailer = require "nodemailer"
|
||||||
|
path = require "path"
|
||||||
|
fs = require "fs"
|
||||||
|
|
||||||
|
watcher = chokidar.watch "./errors", depth: 0, ignoreInitial: true
|
||||||
|
mailer = nodemailer.createTransport()
|
||||||
|
|
||||||
|
processFile = (filePath) ->
|
||||||
|
fs.readFile filePath, (err, data) ->
|
||||||
|
try
|
||||||
|
parsedData = JSON.parse(data)
|
||||||
|
catch error
|
||||||
|
console.log "Error report not complete yet, retrying #{filePath} in 1 second..."
|
||||||
|
setTimeout (->
|
||||||
|
processFile(filePath)
|
||||||
|
), 1000
|
||||||
|
return
|
||||||
|
|
||||||
|
errorMessage = parsedData?.message ? "UNKNOWN ERROR"
|
||||||
|
textStack = parsedData?.stack?.replace(/\u001b(?:\[\??(?:\d\;*)*[A-HJKSTfminsulh])?/g, "") ? ""
|
||||||
|
|
||||||
|
message = """
|
||||||
|
A failure occurred. #{filePath} is attached.
|
||||||
|
|
||||||
|
#{textStack}
|
||||||
|
"""
|
||||||
|
|
||||||
|
htmlMessage = """
|
||||||
|
A failure occurred. #{filePath} is attached.
|
||||||
|
|
||||||
|
<pre>#{textStack}</pre>
|
||||||
|
""".replace(/\n/g, "<br>")
|
||||||
|
|
||||||
|
mailer.sendMail
|
||||||
|
from: "ops@pdf.yt"
|
||||||
|
to: "admin@cryto.net"
|
||||||
|
subject: "Automatic failure report: #{errorMessage}"
|
||||||
|
text: message
|
||||||
|
html: htmlMessage
|
||||||
|
attachments: [
|
||||||
|
filename: path.basename(filePath)
|
||||||
|
path: filePath
|
||||||
|
contentType: "application/json"
|
||||||
|
]
|
||||||
|
|
||||||
|
watcher.on "add", (filePath) ->
|
||||||
|
console.log "PANIC! Sending report:", filePath
|
||||||
|
processFile(filePath)
|
||||||
|
|
||||||
|
console.log "Running..."
|
@ -0,0 +1,35 @@
|
|||||||
|
require "./lib/upload"
|
||||||
|
require "./lib/embed"
|
||||||
|
require "./lib/donate"
|
||||||
|
require "./lib/form-popup"
|
||||||
|
|
||||||
|
$ = require "jquery"
|
||||||
|
autosize = require "autosize"
|
||||||
|
marked = require "marked"
|
||||||
|
scrollFloat = require "./lib/scroll-float"
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
$(".checkAll").on "change", (event) ->
|
||||||
|
newValue = $(this).prop("checked")
|
||||||
|
|
||||||
|
$(this)
|
||||||
|
.closest "table"
|
||||||
|
.find "input[type='checkbox']"
|
||||||
|
.filter ->
|
||||||
|
return !$(this).hasClass "checkAll"
|
||||||
|
.prop "checked", newValue
|
||||||
|
|
||||||
|
scrollFloat($(".floating"))
|
||||||
|
|
||||||
|
autosize($(".md-editor"))
|
||||||
|
|
||||||
|
updatePreview = ->
|
||||||
|
$(".md-preview").html(marked($(this).val()))
|
||||||
|
|
||||||
|
$(".md-editor")
|
||||||
|
.on "change input propertychange", updatePreview
|
||||||
|
.each updatePreview
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
|||||||
|
$ = require "jquery"
|
||||||
|
require "Base64"
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
selectedMethod = undefined
|
||||||
|
selectedAmount = undefined
|
||||||
|
selectedHasPrice = undefined
|
||||||
|
pulsateRemove = undefined
|
||||||
|
|
||||||
|
paypalHandler = (block) ->
|
||||||
|
block.find "input.amount"
|
||||||
|
.val (Math.round(selectedAmount * 100) / 100)
|
||||||
|
|
||||||
|
paymentMethodHandlers =
|
||||||
|
paypal: paypalHandler
|
||||||
|
"paypal-weekly": paypalHandler
|
||||||
|
"paypal-monthly": paypalHandler
|
||||||
|
bitcoin: (block) ->
|
||||||
|
block.find(".loading-message").show()
|
||||||
|
block.find(".loaded-content").hide()
|
||||||
|
|
||||||
|
$.get "/donate/convert/btc?amount=#{selectedAmount}", (btcAmount) ->
|
||||||
|
$.get "/donate/bip21?amount=#{btcAmount}", (uri) ->
|
||||||
|
$(".bip21").attr("href", uri)
|
||||||
|
|
||||||
|
block.find(".loading-message").hide()
|
||||||
|
block.find(".loaded-content").show()
|
||||||
|
|
||||||
|
$(".bip21-qr").attr("src", "/donate/bip21/qr?amount=#{btcAmount}")
|
||||||
|
$(".btc-amount").text(Math.round(btcAmount * 100000000) / 100000000)
|
||||||
|
|
||||||
|
setCustomValue = (value) ->
|
||||||
|
value = parseFloat(value)
|
||||||
|
|
||||||
|
if isNaN value
|
||||||
|
# TODO: Validation error!
|
||||||
|
value = 0
|
||||||
|
|
||||||
|
selectedAmount = value
|
||||||
|
|
||||||
|
$ "#custom_amount_input"
|
||||||
|
.on "keyup", (event) ->
|
||||||
|
setCustomValue $(this).val()
|
||||||
|
showInstructions()
|
||||||
|
.on "change", (event) ->
|
||||||
|
setCustomValue $(this).val()
|
||||||
|
showInstructions()
|
||||||
|
.on "click", (event) ->
|
||||||
|
#$ this
|
||||||
|
# .closest ".option"
|
||||||
|
# .find "input[type='radio']"
|
||||||
|
# .click()
|
||||||
|
|
||||||
|
#event.stopPropagation()
|
||||||
|
|
||||||
|
|
||||||
|
$ "#amount_custom"
|
||||||
|
.on "click", (event) ->
|
||||||
|
setCustomValue $("#custom_amount_input").val()
|
||||||
|
|
||||||
|
$ ".donation-page section.types .option input[type='radio']"
|
||||||
|
.on "click", (event) ->
|
||||||
|
# Hide any instructions that were already visible.
|
||||||
|
# TODO: Automatically show instructions again when switching back...?
|
||||||
|
$ ".donation-page section.instructions div"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
$ ".donation-page section.instructions .placeholder"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
type = $ this
|
||||||
|
.closest ".option"
|
||||||
|
.data "type"
|
||||||
|
|
||||||
|
$ ".donation-page section.methods .option"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
$ ".donation-page section.methods .option[data-type='#{type}']"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
$ ".donation-page section.methods .option input[type='radio']"
|
||||||
|
.on "click", (event) ->
|
||||||
|
optionElement = $ this
|
||||||
|
.closest ".option"
|
||||||
|
|
||||||
|
selectedMethod = method = optionElement.data "name"
|
||||||
|
selectedHasPrice = setPrice = !!(optionElement.data "set-price")
|
||||||
|
|
||||||
|
if setPrice
|
||||||
|
$ ".donation-page section.amount"
|
||||||
|
.slideDown(400)
|
||||||
|
|
||||||
|
instructionSection = $ ".donation-page section.instructions"
|
||||||
|
|
||||||
|
instructionSection
|
||||||
|
.children "h3.set-amount"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
instructionSection
|
||||||
|
.children "h3.no-set-amount"
|
||||||
|
.hide()
|
||||||
|
else
|
||||||
|
$ ".donation-page section.amount"
|
||||||
|
.slideUp(400)
|
||||||
|
|
||||||
|
instructionSection = $ ".donation-page section.instructions"
|
||||||
|
|
||||||
|
instructionSection
|
||||||
|
.children "h3.set-amount"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
instructionSection
|
||||||
|
.children "h3.no-set-amount"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
showInstructions()
|
||||||
|
|
||||||
|
$ ".donation-page section.amount .option input[type='radio']"
|
||||||
|
.on "click", (event) ->
|
||||||
|
if not ($(this).attr("id") == "amount_custom")
|
||||||
|
selectedAmount = $(this).val()
|
||||||
|
|
||||||
|
showInstructions()
|
||||||
|
|
||||||
|
showInstructions = ->
|
||||||
|
$ ".donation-page section.instructions .method"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
if selectedMethod? and (selectedAmount? or !selectedHasPrice)
|
||||||
|
$ ".donation-page section.instructions .placeholder"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
$ ".donation-page section.instructions .method-#{selectedMethod}"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
#$ "html, body"
|
||||||
|
# .animate scrollTop: "#{$('.donation-page section.instructions').offset().top}px", 500
|
||||||
|
|
||||||
|
$ ".donation-page section.instructions"
|
||||||
|
.addClass "pulsate"
|
||||||
|
|
||||||
|
if pulsateRemove?
|
||||||
|
clearTimeout pulsateRemove
|
||||||
|
|
||||||
|
pulsateRemove = setTimeout (->
|
||||||
|
$ ".donation-page section.instructions"
|
||||||
|
.removeClass "pulsate"
|
||||||
|
), 3000
|
||||||
|
|
||||||
|
if selectedMethod of paymentMethodHandlers
|
||||||
|
paymentMethodHandlers[selectedMethod]($(".donation-page section.instructions .method-#{selectedMethod}"))
|
||||||
|
else
|
||||||
|
$ ".donation-page section.instructions .placeholder"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
|
||||||
|
$ ".donation-page .option input[type='radio']"
|
||||||
|
.on "click", (event) ->
|
||||||
|
$ this
|
||||||
|
.closest "section"
|
||||||
|
.find ".option"
|
||||||
|
.removeClass "selected"
|
||||||
|
|
||||||
|
$ this
|
||||||
|
.closest ".option"
|
||||||
|
.addClass "selected"
|
||||||
|
|
||||||
|
$ this
|
||||||
|
.closest ".option"
|
||||||
|
.find "input[type='number']"
|
||||||
|
.focus()
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
$ ".donation-page .option"
|
||||||
|
.on "click", (event) ->
|
||||||
|
$ this
|
||||||
|
.find "input[type='radio']"
|
||||||
|
.click()
|
||||||
|
|
||||||
|
$ ".donation-page section.types .option[data-type='once']"
|
||||||
|
.click()
|
||||||
|
|
||||||
|
$ ".donation-page #amount_10"
|
||||||
|
.click()
|
||||||
|
|
||||||
|
$ ".donation-page section.instructions .method, .donation-page section.instructions h3.no-set-amount"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
$ ".js-unavailable"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
$ ".js-available"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
$ ".decodable"
|
||||||
|
.each ->
|
||||||
|
$(this).html atob($(this).html())
|
||||||
|
|
@ -0,0 +1,33 @@
|
|||||||
|
$ = require "jquery"
|
||||||
|
|
||||||
|
castBoolean = (value) ->
|
||||||
|
if value == true
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updateEmbedCode = ->
|
||||||
|
showToolbar = $("#show_toolbar").prop("checked")
|
||||||
|
showDonationLink = $("#show_donation").prop("checked")
|
||||||
|
|
||||||
|
embedCode = embed_template
|
||||||
|
.replace "{SPARSE}", castBoolean(not showToolbar)
|
||||||
|
.replace "{DONATION}", castBoolean(showDonationLink)
|
||||||
|
|
||||||
|
$(".embed_code").val embedCode
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
$ ".autoselect"
|
||||||
|
.on "click", (event) ->
|
||||||
|
$ this
|
||||||
|
.focus()
|
||||||
|
.select()
|
||||||
|
|
||||||
|
$ "#show_toolbar, #show_donation"
|
||||||
|
.on "change", (event) ->
|
||||||
|
updateEmbedCode()
|
||||||
|
|
||||||
|
# Linkify has a tendency of breaking our embed codes, so we re-set the embed code here to make sure that that doesn't happen.
|
||||||
|
if embed_template?
|
||||||
|
updateEmbedCode()
|
||||||
|
# do things and stuff
|
@ -0,0 +1,16 @@
|
|||||||
|
$ = require "jquery"
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
$(".popup").hide()
|
||||||
|
|
||||||
|
$(".form-popup").each ->
|
||||||
|
elem = $(this)
|
||||||
|
target = ".popup-#{elem.data('popup')}"
|
||||||
|
|
||||||
|
elem.on "click", ->
|
||||||
|
$(target).show()
|
||||||
|
|
||||||
|
$(".popup .close").on "click", ->
|
||||||
|
$(this)
|
||||||
|
.closest ".popup"
|
||||||
|
.hide()
|
@ -0,0 +1,49 @@
|
|||||||
|
$ = require "jquery"
|
||||||
|
|
||||||
|
module.exports = (jqueryObj) ->
|
||||||
|
jqueryObj.each ->
|
||||||
|
elem = $(this)
|
||||||
|
minTop = threshold = basePos = undefined
|
||||||
|
currentlyFloating = false
|
||||||
|
|
||||||
|
originalPositioning = elem.css "position"
|
||||||
|
originalLeft = elem.css "left"
|
||||||
|
originalTop = elem.css "top"
|
||||||
|
originalLeft = elem.css "right"
|
||||||
|
|
||||||
|
updateMetrics = ->
|
||||||
|
needRestore = currentlyFloating
|
||||||
|
|
||||||
|
if needRestore
|
||||||
|
makeNotFloating()
|
||||||
|
|
||||||
|
basePos = elem.offset()
|
||||||
|
minTop = elem.data("min-top") ? 0
|
||||||
|
threshold = basePos.top - minTop
|
||||||
|
|
||||||
|
if needRestore
|
||||||
|
makeFloating()
|
||||||
|
|
||||||
|
makeFloating = ->
|
||||||
|
currentlyFloating = true
|
||||||
|
elem.css
|
||||||
|
position: "fixed"
|
||||||
|
top: "#{minTop}px"
|
||||||
|
right: "auto"
|
||||||
|
left: "#{basePos.left}px"
|
||||||
|
|
||||||
|
makeNotFloating = ->
|
||||||
|
currentlyFloating = false
|
||||||
|
elem.attr "style", ""
|
||||||
|
#elem.css
|
||||||
|
# position: originalPositioning
|
||||||
|
# top: originalTop
|
||||||
|
|
||||||
|
updateMetrics()
|
||||||
|
$(window).on "resize", updateMetrics
|
||||||
|
|
||||||
|
$(document).on "scroll", (event) ->
|
||||||
|
if $(document).scrollTop() >= threshold
|
||||||
|
makeFloating()
|
||||||
|
else
|
||||||
|
makeNotFloating()
|
@ -0,0 +1,176 @@
|
|||||||
|
$ = require "jquery"
|
||||||
|
prettyUnits = require "pretty-units"
|
||||||
|
|
||||||
|
# The AMD loader for this package doesn't work for some reason - so we explicitly disable it. This will force it to fall back to the CommonJS API.
|
||||||
|
require "imports?define=>false!blueimp-file-upload"
|
||||||
|
|
||||||
|
data_object = null
|
||||||
|
|
||||||
|
uploadDone = (response) ->
|
||||||
|
switch response.status
|
||||||
|
when 415
|
||||||
|
errorHeader = "Oops! That's not a PDF file."
|
||||||
|
errorMessage = "The file you tried to upload is not a valid PDF file. Currently, only PDF files are accepted."
|
||||||
|
when 413
|
||||||
|
errorHeader = "Oops! That file is too big."
|
||||||
|
errorMessage = "The file you tried to upload is too big. Currently, you can only upload PDF files up to 150MB in size."
|
||||||
|
when 200 # Nothing, success!
|
||||||
|
else
|
||||||
|
errorHeader = "Oops! Something went wrong."
|
||||||
|
errorMessage = "An unknown error occurred. Please reload the page and try again. If the error keeps occurring, <a href='mailto:pdfy@cryto.net'>send us an e-mail</a>!"
|
||||||
|
|
||||||
|
if errorMessage?
|
||||||
|
triggerError errorHeader, errorMessage
|
||||||
|
reinitializeUploader()
|
||||||
|
else
|
||||||
|
if response.responseJSON.redirect?
|
||||||
|
window.location = response.responseJSON.redirect
|
||||||
|
else
|
||||||
|
# TODO: Wat do?
|
||||||
|
|
||||||
|
triggerError = (header, message) ->
|
||||||
|
$(".upload-form .privacySettings, .upload-form .progress, .upload-form .button-submit").hide()
|
||||||
|
$(".upload").removeClass("faded")
|
||||||
|
|
||||||
|
errorBox = $("#uploadError")
|
||||||
|
.show()
|
||||||
|
|
||||||
|
errorBox.find "h3"
|
||||||
|
.html header
|
||||||
|
|
||||||
|
errorBox.find ".message"
|
||||||
|
.html message
|
||||||
|
|
||||||
|
data_object = null
|
||||||
|
|
||||||
|
reinitializeUploader = ->
|
||||||
|
$("#upload_element").replaceWith($("#upload_element").clone(true))
|
||||||
|
|
||||||
|
filePicked = (data) ->
|
||||||
|
$(".upload-form .privacySettings, .upload-form .button-submit").show()
|
||||||
|
$("#uploadError").hide()
|
||||||
|
$(".upload-form .fileinfo").removeClass("faded")
|
||||||
|
|
||||||
|
fileinfo = $(".fileinfo")
|
||||||
|
filesize = data.files[0].size
|
||||||
|
|
||||||
|
# TODO: Use filesize limit from configuration file!
|
||||||
|
if filesize > (150 * 1024 * 1024)
|
||||||
|
reinitializeUploader()
|
||||||
|
triggerError("Oops! That file is too big.", "The file you tried to upload is too big. Currently, you can only upload PDF files up to 150MB in size.")
|
||||||
|
return
|
||||||
|
|
||||||
|
filesize_text = prettyUnits(filesize) + "B"
|
||||||
|
|
||||||
|
fileinfo.find ".filename"
|
||||||
|
.text data.files[0].name
|
||||||
|
|
||||||
|
fileinfo.find ".filesize"
|
||||||
|
.text filesize_text
|
||||||
|
|
||||||
|
$ ".info"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
fileinfo
|
||||||
|
.show()
|
||||||
|
|
||||||
|
$ ".upload"
|
||||||
|
.addClass "faded"
|
||||||
|
|
||||||
|
updateUploadProgress = (event) ->
|
||||||
|
if event.lengthComputable
|
||||||
|
percentage = event.loaded / event.total * 100
|
||||||
|
|
||||||
|
done_text = prettyUnits(event.loaded) + "B"
|
||||||
|
total_text = prettyUnits(event.total) + "B"
|
||||||
|
|
||||||
|
progress = $ ".progress"
|
||||||
|
|
||||||
|
progress.find ".done"
|
||||||
|
.text done_text
|
||||||
|
|
||||||
|
progress.find ".total"
|
||||||
|
.text total_text
|
||||||
|
|
||||||
|
progress.find ".percentage"
|
||||||
|
.text (Math.ceil(percentage * 100) / 100)
|
||||||
|
|
||||||
|
progress.find ".bar-inner"
|
||||||
|
.css width: "#{percentage}%"
|
||||||
|
|
||||||
|
if event.loaded >= event.total
|
||||||
|
# Completed!
|
||||||
|
progress.find ".numbers"
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
progress.find ".wait"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
if $().fileupload?
|
||||||
|
# Only run this if the fileupload plugin is loaded; we don't need all this on eg. the 'view' page.
|
||||||
|
|
||||||
|
$ "#upload_form"
|
||||||
|
.fileupload
|
||||||
|
fileInput: null
|
||||||
|
type: "POST"
|
||||||
|
url: "/upload"
|
||||||
|
paramName: "file"
|
||||||
|
autoUpload: false
|
||||||
|
maxNumberOfFiles: 1
|
||||||
|
formData: (form) ->
|
||||||
|
form = $ "#upload_form"
|
||||||
|
form.serializeArray()
|
||||||
|
progressall: (e, data) ->
|
||||||
|
updateUploadProgress
|
||||||
|
lengthComputable: true
|
||||||
|
loaded: data.loaded
|
||||||
|
total: data.total
|
||||||
|
add: (e, data) ->
|
||||||
|
data_object = data
|
||||||
|
filePicked(data)
|
||||||
|
always: (e, data) ->
|
||||||
|
uploadDone(data.jqXHR)
|
||||||
|
|
||||||
|
$ "#upload_activator"
|
||||||
|
.on "click", (event) ->
|
||||||
|
$("#upload_element").click()
|
||||||
|
|
||||||
|
$ "#upload_element"
|
||||||
|
.on "change", (event) ->
|
||||||
|
filePicked(this)
|
||||||
|
|
||||||
|
$ "#upload_form"
|
||||||
|
.on "submit", (event) ->
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
$ ".fileinfo"
|
||||||
|
.addClass "faded"
|
||||||
|
|
||||||
|
$ ".progress"
|
||||||
|
.show()
|
||||||
|
|
||||||
|
if data_object == null
|
||||||
|
# Only do this if the drag-and-drop dropzone hasn't been used.
|
||||||
|
formData = new FormData(this)
|
||||||
|
|
||||||
|
$.ajax
|
||||||
|
method: "POST"
|
||||||
|
url: "/upload"
|
||||||
|
data: formData
|
||||||
|
cache: false
|
||||||
|
contentType: false
|
||||||
|
processData: false
|
||||||
|
xhr: ->
|
||||||
|
customHandler = $.ajaxSettings.xhr()
|
||||||
|
|
||||||
|
if customHandler.upload?
|
||||||
|
customHandler.upload.addEventListener "progress", updateUploadProgress, false
|
||||||
|
|
||||||
|
return customHandler
|
||||||
|
complete: (result) ->
|
||||||
|
uploadDone(result)
|
||||||
|
else
|
||||||
|
# If the dropzone was used...
|
||||||
|
data_object.submit()
|
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env coffee
|
||||||
|
|
||||||
|
scrypt = require "scrypt-for-humans"
|
||||||
|
Promise = require "bluebird"
|
||||||
|
read = Promise.promisify(require "read")
|
||||||
|
|
||||||
|
Promise.try ->
|
||||||
|
read(prompt: "Enter a password:", silent: true)
|
||||||
|
.spread (password, isDefault) ->
|
||||||
|
if password.trim().length == 0
|
||||||
|
console.log "You didn't enter a password!"
|
||||||
|
process.exit(1)
|
||||||
|
|
||||||
|
scrypt.hash(password)
|
||||||
|
.then (hash) ->
|
||||||
|
console.log "Hash:", hash
|
||||||
|
console.log "Set this hash in your config.json to use it."
|
@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="12.000021"
|
||||||
|
height="13.574016"
|
||||||
|
viewBox="0 0 12.000021 13.574015"
|
||||||
|
enable-background="new 0 0 599.998 583.111"
|
||||||
|
xml:space="preserve"
|
||||||
|
id="svg3039"
|
||||||
|
inkscape:version="0.48.4 r9939"
|
||||||
|
sodipodi:docname="ia-icon.svg"><metadata
|
||||||
|
id="metadata3096"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs3094" /><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1125"
|
||||||
|
id="namedview3092"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="7.9867624"
|
||||||
|
inkscape:cx="34.68682"
|
||||||
|
inkscape:cy="12.715741"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="A" /><g
|
||||||
|
id="A"
|
||||||
|
transform="translate(-140.28052,-561.46621)"><rect
|
||||||
|
x="140.28052"
|
||||||
|
y="574.42487"
|
||||||
|
width="12.000021"
|
||||||
|
height="0.61536318"
|
||||||
|
id="rect3058"
|
||||||
|
style="fill:#ffffff" /><rect
|
||||||
|
x="140.75385"
|
||||||
|
y="573.24133"
|
||||||
|
width="11.076934"
|
||||||
|
height="0.8520959"
|
||||||
|
id="rect3060"
|
||||||
|
style="fill:#ffffff" /><rect
|
||||||
|
x="140.68288"
|
||||||
|
y="563.53723"
|
||||||
|
width="11.017765"
|
||||||
|
height="1.1834365"
|
||||||
|
id="rect3062"
|
||||||
|
style="fill:#ffffff" /><polygon
|
||||||
|
points="347.701,162.012 539.506,162.012 551.193,149.072 347.701,101.486 144.21,149.072 155.897,162.012 "
|
||||||
|
id="polygon3064"
|
||||||
|
transform="matrix(0.02835123,0,0,0.02835123,136.33399,558.58896)"
|
||||||
|
style="fill:#ffffff" /><path
|
||||||
|
d="m 142.27205,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0474,-1.74323 -0.0213,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0222,-0.0574 -0.0615,-0.066 -0.1614,-0.0351 -0.32366,-0.0517 -0.4866,-0.052 -0.16294,2.8e-4 -0.32517,0.0169 -0.48659,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0266,0.54706 -0.0618,1.09375 -0.0832,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.18084,0.047 0.36017,0.0742 0.53937,0.0766 0.17922,-9.3e-4 0.35851,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0222,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0287,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
|
||||||
|
id="path3084"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff" /><path
|
||||||
|
d="m 145.27192,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0474,-1.74323 -0.0214,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0223,-0.0574 -0.0615,-0.066 -0.16139,-0.0351 -0.32366,-0.0517 -0.48656,-0.052 -0.16295,2.8e-4 -0.3252,0.0169 -0.4866,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0265,0.54706 -0.0618,1.09375 -0.0832,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.18089,0.047 0.3602,0.0742 0.53938,0.0766 0.17921,-9.3e-4 0.35849,-0.0294 0.53939,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0222,-0.27872 0.0268,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0286,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
|
||||||
|
id="path3086"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff" /><path
|
||||||
|
d="m 148.75103,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0473,-1.74323 -0.0214,-0.54724 -0.0566,-1.09393 -0.0832,-1.64099 -9.4e-4,-0.0473 -0.0223,-0.0574 -0.0616,-0.066 -0.16137,-0.0351 -0.32363,-0.0517 -0.48656,-0.052 -0.16293,2.8e-4 -0.32518,0.0169 -0.48661,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0265,0.54706 -0.0618,1.09375 -0.0833,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0505,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0222,0.27872 0.18088,0.047 0.36017,0.0742 0.53938,0.0766 0.17919,-9.3e-4 0.3585,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0221,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0287,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
|
||||||
|
id="path3088"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff" /><path
|
||||||
|
d="m 151.67987,568.4671 c -0.009,-0.58121 -0.0246,-1.16243 -0.0474,-1.74323 -0.0214,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0222,-0.0574 -0.0615,-0.066 -0.16135,-0.0351 -0.32364,-0.0517 -0.48657,-0.052 -0.1629,2.8e-4 -0.32518,0.0169 -0.48655,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0266,0.54706 -0.0618,1.09375 -0.0833,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.1809,0.047 0.3602,0.0742 0.53938,0.0766 0.17921,-9.3e-4 0.35852,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0221,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0286,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
|
||||||
|
id="path3090"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#ffffff" /></g></svg>
|
After Width: | Height: | Size: 6.1 KiB |
@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="180"
|
||||||
|
height="261"
|
||||||
|
id="svg4889"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.48.4 r9939"
|
||||||
|
sodipodi:docname="no-thumbnail.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4891" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="4"
|
||||||
|
inkscape:cx="88.419794"
|
||||||
|
inkscape:cy="5.8296482"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1125"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata4894">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-791.36218)">
|
||||||
|
<g
|
||||||
|
id="g5420"
|
||||||
|
transform="translate(4.8995525,0)">
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5414"
|
||||||
|
d="m 32.763905,870.12363 0,98.58436 104.673085,0 0,-141.62912 -56.606425,0 -48.06666,43.04476 z"
|
||||||
|
style="fill:#f3f2f2;fill-opacity:1;stroke:#e3e3e3;stroke-width:1.14982712;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="rect4901"
|
||||||
|
d="m 80.830565,827.07887 -48.06666,43.04476 48.06666,0 0,-43.04476 z"
|
||||||
|
style="fill:#f3f2f2;fill-opacity:1;stroke:#e3e3e3;stroke-width:1.05590618;stroke-linejoin:bevel;stroke-opacity:1" />
|
||||||
|
<text
|
||||||
|
sodipodi:linespacing="125%"
|
||||||
|
id="text5416"
|
||||||
|
y="949.12549"
|
||||||
|
x="85.945068"
|
||||||
|
style="font-size:78.12385559px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#e0e0e0;fill-opacity:1;stroke:none;font-family:Sans"
|
||||||
|
xml:space="preserve"><tspan
|
||||||
|
y="949.12549"
|
||||||
|
x="85.945068"
|
||||||
|
id="tspan5418"
|
||||||
|
sodipodi:role="line">?</tspan></text>
|
||||||
|
</g>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:31.59504318px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#b3b3b3;fill-opacity:1;stroke:none;font-family:Sans"
|
||||||
|
x="19.971004"
|
||||||
|
y="1014.6339"
|
||||||
|
id="text5426"
|
||||||
|
sodipodi:linespacing="125%"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan5428"
|
||||||
|
x="19.971004"
|
||||||
|
y="1014.6339"
|
||||||
|
style="font-size:18.95702553px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#b3b3b3;font-family:Luxi Sans;-inkscape-font-specification:Luxi Sans">no thumbnail yet</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
@ -0,0 +1,85 @@
|
|||||||
|
var gulp = require('gulp');
|
||||||
|
|
||||||
|
/* CoffeeScript compile deps */
|
||||||
|
var path = require('path');
|
||||||
|
var gutil = require('gulp-util');
|
||||||
|
var concat = require('gulp-concat');
|
||||||
|
var rename = require('gulp-rename');
|
||||||
|
var coffee = require('gulp-coffee');
|
||||||
|
var cache = require('gulp-cached');
|
||||||
|
var remember = require('gulp-remember');
|
||||||
|
var plumber = require('gulp-plumber');
|
||||||
|
var livereload = require('gulp-livereload');
|
||||||
|
var nodemon = require("gulp-nodemon");
|
||||||
|
var net = require("net");
|
||||||
|
var webpack = require("gulp-webpack");
|
||||||
|
var sass = require("gulp-sass");
|
||||||
|
|
||||||
|
task = {
|
||||||
|
"source": ["public/**/*.coffee", "routes/**/*.coffee", "models/**/*.coffee", "tasks/**/*.coffee", "app.coffee", "util.coffee", "migrate.coffee", "db.coffee"]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
gulp.task('coffee', function() {
|
||||||
|
return gulp.src(task.source, {base: "."})
|
||||||
|
.pipe(plumber())
|
||||||
|
.pipe(cache("coffee"))
|
||||||
|
.pipe(coffee({bare: true}).on('error', gutil.log)).on('data', gutil.log)
|
||||||
|
.pipe(remember("coffee"))
|
||||||
|
.pipe(gulp.dest("."));
|
||||||
|
});*/
|
||||||
|
|
||||||
|
gulp.task('webpack', function(){
|
||||||
|
return gulp.src("frontend/index.coffee")
|
||||||
|
.pipe(webpack({
|
||||||
|
watch: true,
|
||||||
|
module: {
|
||||||
|
loaders: [{ test: /\.coffee$/, loader: "coffee-loader" }]
|
||||||
|
},
|
||||||
|
resolve: { extensions: ["", ".web.coffee", ".web.js", ".coffee", ".js"] }
|
||||||
|
}))
|
||||||
|
.pipe(rename("bundle.js"))
|
||||||
|
.pipe(gulp.dest("public/js/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('sass', function(){
|
||||||
|
// TODO: Put the source SCSS file in a more logical place...
|
||||||
|
return gulp.src("./public/css/*.scss")
|
||||||
|
.pipe(sass())
|
||||||
|
.pipe(gulp.dest("./public/css"));
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkServerUp(){
|
||||||
|
setTimeout(function(){
|
||||||
|
var sock = new net.Socket();
|
||||||
|
sock.setTimeout(50);
|
||||||
|
sock.on("connect", function(){
|
||||||
|
console.log("Trigger page reload...");
|
||||||
|
livereload.changed();
|
||||||
|
sock.destroy();
|
||||||
|
})
|
||||||
|
.on("timeout", checkServerUp)
|
||||||
|
.on("error", checkServerUp)
|
||||||
|
.connect(3000);
|
||||||
|
}, 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
gulp.task('watch', function () {
|
||||||
|
livereload.listen();
|
||||||
|
gulp.watch(['./**/*.css', 'views/**/*.jade', 'package.json', "./public/js/**/*.js"]).on('change', livereload.changed);
|
||||||
|
gulp.watch(["./public/css/style.scss"], ["sass"])
|
||||||
|
//gulp.watch(task.source, ['coffee']);
|
||||||
|
// theseus disabled for now, it was screwing with my tracebacks
|
||||||
|
//nodemon({script: "./bin/www", ext: "js", nodeArgs: ['/usr/bin/node-theseus']}).on("start", checkServerUp);
|
||||||
|
nodemon({
|
||||||
|
script: "./bin/www.coffee",
|
||||||
|
ext: "coffee",
|
||||||
|
delay: 500,
|
||||||
|
ignore: ["./frontend/"],
|
||||||
|
watch: ["app.coffee", "bin", "lib", "models", "routes", "tasks"]
|
||||||
|
}).on("start", checkServerUp).on("restart", function(file){
|
||||||
|
console.log("Restarted triggered by:", file);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('default', [/*'coffee',*/ 'sass', 'watch', 'webpack']);
|
@ -0,0 +1,17 @@
|
|||||||
|
# Update with your config settings.
|
||||||
|
|
||||||
|
config = require "./config.json"
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
# TODO: Do we need an environment name here?
|
||||||
|
development:
|
||||||
|
client: "mysql2"
|
||||||
|
connection:
|
||||||
|
database: config.database.database
|
||||||
|
user: config.database.username
|
||||||
|
password: config.database.password
|
||||||
|
pool:
|
||||||
|
min: 2
|
||||||
|
max: 10
|
||||||
|
migrations:
|
||||||
|
tableName: "knex_migrations"
|
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = (req, res, next) ->
|
||||||
|
if (not res.locals.maintenanceMode) or req.session.isAdmin
|
||||||
|
next()
|
||||||
|
else
|
||||||
|
res.status(503).send(res.locals.maintenanceModeText)
|
@ -0,0 +1,25 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
bhttp = require "bhttp"
|
||||||
|
|
||||||
|
lastRates = null
|
||||||
|
lastRateCheck = 0
|
||||||
|
|
||||||
|
module.exports = ->
|
||||||
|
Promise.try ->
|
||||||
|
if Date.now() > lastRateCheck + (5 * 60 * 1000)
|
||||||
|
# We need fresh API data, 5 minutes have elapsed.
|
||||||
|
Promise.try ->
|
||||||
|
Promise.all [
|
||||||
|
bhttp.get "http://api.fixer.io/latest", decodeJSON: true
|
||||||
|
bhttp.get "https://blockchain.info/ticker", decodeJSON: true
|
||||||
|
]
|
||||||
|
.spread (fixerRates, blockchainRates) ->
|
||||||
|
eurRates = fixerRates.body.rates
|
||||||
|
eurRates.BTC = 1 / blockchainRates.body.EUR["15m"]
|
||||||
|
Promise.resolve eurRates
|
||||||
|
.then (rates) ->
|
||||||
|
lastRates = rates
|
||||||
|
lastRateCheck = Date.now()
|
||||||
|
Promise.resolve rates
|
||||||
|
else
|
||||||
|
Promise.resolve lastRates
|
@ -0,0 +1,7 @@
|
|||||||
|
errors = require "errors"
|
||||||
|
|
||||||
|
module.exports = (req, res, next) ->
|
||||||
|
if req.session?.isAdmin?
|
||||||
|
next()
|
||||||
|
else
|
||||||
|
next(new errors.NotAuthenticated("You are not logged in as an administrator."))
|
@ -0,0 +1,11 @@
|
|||||||
|
errors = require "errors"
|
||||||
|
|
||||||
|
amountRegex = /^[0-9]+(?:\.[0-9]+)?$/
|
||||||
|
|
||||||
|
module.exports = (amount) ->
|
||||||
|
parsedAmount = parseFloat(amount)
|
||||||
|
|
||||||
|
if amountRegex.exec(amount) == null or isNaN(parsedAmount)
|
||||||
|
throw new errors.InvalidInput("The specified amount is invalid.")
|
||||||
|
|
||||||
|
return parsedAmount
|
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = (param) ->
|
||||||
|
if not param?
|
||||||
|
return undefined
|
||||||
|
else
|
||||||
|
return !!(parseInt(param))
|
@ -0,0 +1,71 @@
|
|||||||
|
# NOTE: This module does not currently ensure correct writes. Callbacks are called immediately (but asynchronously).
|
||||||
|
|
||||||
|
AbstractClientStore = require "express-brute/lib/AbstractClientStore"
|
||||||
|
|
||||||
|
module.exports = class PersistBruteStore extends AbstractClientStore
|
||||||
|
constructor: (options) ->
|
||||||
|
@persist = options.persist
|
||||||
|
@prefix = options.prefix ? "brute"
|
||||||
|
|
||||||
|
@_timers = {}
|
||||||
|
@_keyMatcher = new RegExp("^#{@prefix}:")
|
||||||
|
|
||||||
|
@persist.keys()
|
||||||
|
.filter (key) => key.match(@_keyMatcher)
|
||||||
|
.forEach (key) =>
|
||||||
|
@_createExpiryTimer key, @persist.getItem(key).expiry
|
||||||
|
|
||||||
|
_createExpiryTimer: (key, expiry) ->
|
||||||
|
@_removeExpiryTimer(key)
|
||||||
|
|
||||||
|
ttl = expiry - Date.now()
|
||||||
|
|
||||||
|
if ttl < 0
|
||||||
|
@_createRemover(key)()
|
||||||
|
else
|
||||||
|
setTimeout @_createRemover(key), ttl
|
||||||
|
|
||||||
|
_removeExpiryTimer: (key) ->
|
||||||
|
if @_timers[key]?
|
||||||
|
clearTimeout @_timers[key]
|
||||||
|
delete @_timers[key]
|
||||||
|
|
||||||
|
_createRemover: (key) ->
|
||||||
|
return =>
|
||||||
|
@_removeExpiryTimer(key)
|
||||||
|
|
||||||
|
prefixedKey = "#{@prefix}:#{key}"
|
||||||
|
|
||||||
|
# TODO: Error handling?
|
||||||
|
@persist.removeItem prefixedKey
|
||||||
|
|
||||||
|
set: (key, value, lifetime, callback) ->
|
||||||
|
prefixedKey = "#{@prefix}:#{key}"
|
||||||
|
expiry = (Date.now() + lifetime)
|
||||||
|
|
||||||
|
@_createExpiryTimer key, expiry
|
||||||
|
@persist.setItem prefixedKey, {value: value, expiry: expiry}, callback
|
||||||
|
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null)
|
||||||
|
|
||||||
|
get: (key, callback) ->
|
||||||
|
prefixedKey = "#{@prefix}:#{key}"
|
||||||
|
result = @persist.getItem(prefixedKey)
|
||||||
|
value = result?.value
|
||||||
|
|
||||||
|
# Normalize to dates if we're reading from disk-persisted data... on disk, they're saved as strings.
|
||||||
|
if value?.firstRequest? and value?.firstRequest not instanceof Date
|
||||||
|
value.firstRequest = new Date(value.firstRequest)
|
||||||
|
|
||||||
|
if value?.lastRequest? and value?.lastRequest not instanceof Date
|
||||||
|
value.lastRequest = new Date(value.lastRequest)
|
||||||
|
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null, value)
|
||||||
|
|
||||||
|
reset: (key, callback) ->
|
||||||
|
# I don't really understand why this is called 'reset'. It's quite clearly a 'remove' function...
|
||||||
|
@_createRemover(key)()
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null)
|
@ -0,0 +1,40 @@
|
|||||||
|
# This will break horribly in a multi-process setup! Don't do that!
|
||||||
|
# NOTE: Does not currently ensure writes.
|
||||||
|
|
||||||
|
module.exports = (session) ->
|
||||||
|
class PersistSessionStore extends session.Store
|
||||||
|
constructor: (options) ->
|
||||||
|
@persist = options.persist
|
||||||
|
|
||||||
|
get: (sid, callback) ->
|
||||||
|
sessionData = @persist.getItem "session:#{sid}"
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null, sessionData)
|
||||||
|
|
||||||
|
set: (sid, sessionData, callback) ->
|
||||||
|
sessionData.__lastAccess = Date.now()
|
||||||
|
@persist.setItem "session:#{sid}", sessionData
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null)
|
||||||
|
|
||||||
|
destroy: (sid, session, callback) ->
|
||||||
|
@persist.removeItem "session:#{sid}"
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null)
|
||||||
|
|
||||||
|
length: (callback) ->
|
||||||
|
length = @persist.valuesWithKeyMatch(/^session:/).length
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null, length)
|
||||||
|
|
||||||
|
clear: (callback) ->
|
||||||
|
@persist.keys()
|
||||||
|
.filter (key) -> key.match(/^session:/)
|
||||||
|
.forEach (key) ->
|
||||||
|
@persist.removeItem key
|
||||||
|
|
||||||
|
process.nextTick ->
|
||||||
|
callback(null)
|
||||||
|
|
||||||
|
# TODO: .touch
|
||||||
|
|
@ -0,0 +1,74 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
persist = require "node-persist"
|
||||||
|
path = require "path"
|
||||||
|
xtend = require "xtend"
|
||||||
|
|
||||||
|
# We MUST explicitly specify the `persist` directory, otherwise node-persist will bug out and write to its own module directory...
|
||||||
|
persist.initSync(continuous: false, dir: path.join(__dirname, "../persist"))
|
||||||
|
|
||||||
|
persist.increment = (key, amount = 1) ->
|
||||||
|
persist.setItem key, (persist.getItem(key) + amount)
|
||||||
|
|
||||||
|
persist.decrement = (key, amount = 1) ->
|
||||||
|
persist.setItem key, (persist.getItem(key) - amount)
|
||||||
|
|
||||||
|
persist.addListItem = (key, item) ->
|
||||||
|
newList = [item].concat (persist.getItem(key) ? [])
|
||||||
|
|
||||||
|
persist.setItem key, newList
|
||||||
|
|
||||||
|
persist.removeListItem = (key, item) ->
|
||||||
|
newList = (persist.getItem(key) ? [])
|
||||||
|
.filter (existingItem) ->
|
||||||
|
return (item == existingItem)
|
||||||
|
|
||||||
|
persist.setItem key, newList
|
||||||
|
|
||||||
|
persist.removeListItemByFilter = (key, filter) ->
|
||||||
|
newList = (persist.getItem(key) ? [])
|
||||||
|
.filter (item) ->
|
||||||
|
return !filter(item)
|
||||||
|
|
||||||
|
persist.setItem key, newList
|
||||||
|
|
||||||
|
persist.setProperty = (key, propertyKey, value) ->
|
||||||
|
newObj = {}
|
||||||
|
newObj[propertyKey] = value
|
||||||
|
oldObj = persist.get(key)
|
||||||
|
|
||||||
|
persist.setItem key, xtend(oldObj, newObj)
|
||||||
|
|
||||||
|
persist.removeProperty = (key, propertyKey) ->
|
||||||
|
# Extremely ghetto shallow clone
|
||||||
|
obj = xtend({}, persist.get(key))
|
||||||
|
delete obj[propertyKey]
|
||||||
|
|
||||||
|
persist.setItem key, obj
|
||||||
|
|
||||||
|
# Rough shim for write queueing...
|
||||||
|
writeQueue = []
|
||||||
|
currentlyWriting = false
|
||||||
|
_setItem = persist.setItem
|
||||||
|
|
||||||
|
persist.setItem = (key, value) ->
|
||||||
|
new Promise (resolve, reject) ->
|
||||||
|
_setItem.call(persist, key, value)
|
||||||
|
addItemToQueue key, value, resolve, reject
|
||||||
|
triggerWrite()
|
||||||
|
|
||||||
|
addItemToQueue = (key, value, resolveFunc, rejectFunc) ->
|
||||||
|
writeQueue.push [key, value, resolveFunc, rejectFunc]
|
||||||
|
|
||||||
|
triggerWrite = ->
|
||||||
|
if not currentlyWriting and writeQueue.length > 0
|
||||||
|
currentlyWriting = 1
|
||||||
|
[key, value, resolveFunc, rejectFunc] = writeQueue.shift()
|
||||||
|
|
||||||
|
Promise.resolve(persist.persistKey(key))
|
||||||
|
.then (result) -> resolveFunc(result)
|
||||||
|
.catch (err) -> rejectFunc(err)
|
||||||
|
.finally ->
|
||||||
|
currentlyWriting = false
|
||||||
|
triggerWrite()
|
||||||
|
|
||||||
|
module.exports = persist
|
@ -0,0 +1,15 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
crypto = Promise.promisifyAll(require "crypto")
|
||||||
|
|
||||||
|
module.exports = (length = 16) ->
|
||||||
|
Promise.try ->
|
||||||
|
byteLength = Math.ceil(length / 4) * 3
|
||||||
|
return crypto.randomBytesAsync(byteLength)
|
||||||
|
.then (bytes) ->
|
||||||
|
bytes = bytes
|
||||||
|
.toString "base64"
|
||||||
|
.replace /\+/g, "-"
|
||||||
|
.replace /\//g, "_"
|
||||||
|
.slice 0, length
|
||||||
|
|
||||||
|
Promise.resolve bytes
|
@ -0,0 +1,34 @@
|
|||||||
|
class RateLimiter
|
||||||
|
constructor: (@limit, @interval, @funcA, @funcB) ->
|
||||||
|
@_totalCalls = 0
|
||||||
|
@_startTimer()
|
||||||
|
_startTimer: ->
|
||||||
|
@_timer = setInterval @_clearCalls, @interval
|
||||||
|
_stopTimer: ->
|
||||||
|
if @_timer?
|
||||||
|
clearInterval @_timer
|
||||||
|
@_timer = null
|
||||||
|
_clearCalls: ->
|
||||||
|
@_totalCalls = 0
|
||||||
|
call: ->
|
||||||
|
@_totalCalls += 1
|
||||||
|
|
||||||
|
targetFunc = switch
|
||||||
|
when @_totalCalls <= @limit then @funcA
|
||||||
|
else @funcB
|
||||||
|
|
||||||
|
targetFunc.apply this, arguments
|
||||||
|
setInterval: (interval) ->
|
||||||
|
@_stopTimer()
|
||||||
|
@interval = interval
|
||||||
|
@_startTimer()
|
||||||
|
setLimit: (limit) ->
|
||||||
|
@limit = limit
|
||||||
|
|
||||||
|
module.exports = ->
|
||||||
|
return (funcA, funcB, options) ->
|
||||||
|
if not options.limit?
|
||||||
|
throw new Error("No limit specified.")
|
||||||
|
options.interval ?= 60 # Default: 60 seconds ie. 1 minute.
|
||||||
|
|
||||||
|
return new RateLimiter(options.limit, options.interval, funcA, funcB)
|
@ -0,0 +1,12 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
concatStream = require "concat-stream"
|
||||||
|
buffertools = require "buffertools"
|
||||||
|
|
||||||
|
module.exports = (stream, needle) ->
|
||||||
|
# CAUTION: This buffers up in memory!
|
||||||
|
new Promise (resolve, reject) ->
|
||||||
|
stream
|
||||||
|
.pipe concatStream (result) ->
|
||||||
|
resolve buffertools.indexOf(result, needle) != -1
|
||||||
|
.on "error", (err) ->
|
||||||
|
reject err
|
@ -0,0 +1,13 @@
|
|||||||
|
# NOTE: This is purely a `tap` equivalent for errors! Any resolves or rejections are ignored - promises can only be used to wait for async execution of something.
|
||||||
|
|
||||||
|
Promise = require "bluebird"
|
||||||
|
|
||||||
|
module.exports = (func) ->
|
||||||
|
return (err) ->
|
||||||
|
Promise.try ->
|
||||||
|
func(err)
|
||||||
|
.catch ->
|
||||||
|
# Consume any errors
|
||||||
|
Promise.resolve()
|
||||||
|
.then ->
|
||||||
|
Promise.reject(err)
|
@ -0,0 +1,95 @@
|
|||||||
|
EventEmitter = require("events").EventEmitter
|
||||||
|
Promise = require "bluebird"
|
||||||
|
debug = require("debug")("task-runner")
|
||||||
|
|
||||||
|
makeExternalPromise = ->
|
||||||
|
extResolve = extReject = null
|
||||||
|
new Promise (resolve, reject) ->
|
||||||
|
extResolve = resolve
|
||||||
|
extReject = reject
|
||||||
|
|
||||||
|
module.exports = class TaskRunner extends EventEmitter
|
||||||
|
constructor: (@context = {}) ->
|
||||||
|
@_taskTypes = {}
|
||||||
|
@_queue = {}
|
||||||
|
@_runningCount = {}
|
||||||
|
@running = false
|
||||||
|
@context.taskRunner = this
|
||||||
|
|
||||||
|
_checkRunTask: ->
|
||||||
|
if not @running
|
||||||
|
return
|
||||||
|
|
||||||
|
debug "checking for runnable tasks..."
|
||||||
|
|
||||||
|
for taskType, options of @_taskTypes
|
||||||
|
if @_runningCount[taskType] < (options.maxConcurrent ? Infinity)
|
||||||
|
if options.maxConcurrent?
|
||||||
|
tasksToRun = options.maxConcurrent - @_runningCount[taskType]
|
||||||
|
else
|
||||||
|
tasksToRun = Infinity
|
||||||
|
|
||||||
|
debug "running #{tasksToRun} tasks"
|
||||||
|
|
||||||
|
for i in [0...tasksToRun]
|
||||||
|
if @_queue[taskType].length == 0
|
||||||
|
debug "ran out of tasks"
|
||||||
|
@emit "tasksDepleted"
|
||||||
|
break
|
||||||
|
|
||||||
|
@_doRunTask taskType, @_queue[taskType].shift()
|
||||||
|
|
||||||
|
debug "started #{tasksToRun} tasks, waiting for completion..."
|
||||||
|
|
||||||
|
_doRunTask: (taskType, taskData) ->
|
||||||
|
taskOptions = @_taskTypes[taskType]
|
||||||
|
@_runningCount[taskType] += 1
|
||||||
|
@emit "taskStarted", taskType, taskData.task
|
||||||
|
|
||||||
|
Promise.resolve(taskOptions.taskFunc(taskData.task, @context))
|
||||||
|
.then (value) =>
|
||||||
|
@_markTaskCompleted taskType, taskData
|
||||||
|
taskData.resolveFunc(value)
|
||||||
|
.catch (err) =>
|
||||||
|
@emit "taskFailed", taskType, taskData.task, err
|
||||||
|
taskData.rejectFunc(err)
|
||||||
|
|
||||||
|
debug "started task"
|
||||||
|
|
||||||
|
_markTaskCompleted: (taskType, task) ->
|
||||||
|
@_runningCount[taskType] -= 1
|
||||||
|
@emit "taskCompleted", taskType, task.task
|
||||||
|
debug "completed task"
|
||||||
|
@_checkRunTask()
|
||||||
|
|
||||||
|
addTask: (taskType, taskFunc, options = {}) =>
|
||||||
|
options.taskFunc = taskFunc
|
||||||
|
@_taskTypes[taskType] = options
|
||||||
|
@_queue[taskType] = []
|
||||||
|
@_runningCount[taskType] = 0
|
||||||
|
debug "added task"
|
||||||
|
|
||||||
|
setTaskOptions: (taskType, options) =>
|
||||||
|
options.taskFunc = @_taskTypes[taskType].taskFunc
|
||||||
|
@_taskTypes[taskType] = options
|
||||||
|
|
||||||
|
do: (taskType, task) =>
|
||||||
|
@emit "taskQueued", taskType, task
|
||||||
|
debug "queued task"
|
||||||
|
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
@_queue[taskType].push
|
||||||
|
resolveFunc: resolve
|
||||||
|
rejectFunc: reject
|
||||||
|
task: task
|
||||||
|
|
||||||
|
@_checkRunTask()
|
||||||
|
|
||||||
|
run: =>
|
||||||
|
@running = true
|
||||||
|
debug "started task loop"
|
||||||
|
@_checkRunTask()
|
||||||
|
|
||||||
|
pause: =>
|
||||||
|
@running = false
|
||||||
|
debug "paused task loop"
|
@ -0,0 +1,15 @@
|
|||||||
|
moment = require "moment"
|
||||||
|
|
||||||
|
module.exports = (req, res, next) ->
|
||||||
|
res.locals.conditionalClasses = (always, conditionals) ->
|
||||||
|
applicableConditionals = (className for className, condition of conditionals when condition)
|
||||||
|
applicableClasses = always.concat applicableConditionals
|
||||||
|
return applicableClasses.join " "
|
||||||
|
|
||||||
|
res.locals.makeBreakable = (string) ->
|
||||||
|
require("jade/lib/runtime").escape(string).replace(/_/g, "_<wbr>")
|
||||||
|
|
||||||
|
res.locals.shortDate = (date) ->
|
||||||
|
moment(date).format "MMM Do, YYYY hh:mm:ss"
|
||||||
|
|
||||||
|
next()
|
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = (req, res, next) ->
|
||||||
|
token = req.csrfToken()
|
||||||
|
res.locals.csrfToken = token
|
||||||
|
next()
|
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.boolean "CDN"
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.dropColumn "CDN"
|
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.boolean "Thumbnailed"
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.dropColumn "Thumbnailed"
|
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
knex.schema.raw "ALTER TABLE documents ADD INDEX (Public)"
|
||||||
|
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
#
|
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
knex.schema.createTable "blog_posts", (table) ->
|
||||||
|
table.bigIncrements("Id")
|
||||||
|
table.string("Slug")
|
||||||
|
table.string("Title")
|
||||||
|
table.text("Body", "longtext")
|
||||||
|
table.timestamp("Posted").nullable()
|
||||||
|
table.timestamp("Edited").nullable()
|
||||||
|
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
knex.schema.dropTable "blog_posts"
|
@ -0,0 +1,61 @@
|
|||||||
|
rfr = require "rfr"
|
||||||
|
persist = rfr "lib/persist"
|
||||||
|
|
||||||
|
initializeVariable = (name, type, initialValue) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.addListItem "variableTypes",
|
||||||
|
name: name
|
||||||
|
type: type
|
||||||
|
|
||||||
|
persist.setItem "var:#{name}", initialValue
|
||||||
|
]
|
||||||
|
|
||||||
|
removeVariable = (name) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.removeListItemByFilter "variableTypes", (item) ->
|
||||||
|
return (item.name == name)
|
||||||
|
|
||||||
|
persist.removeItem "var:#{name}"
|
||||||
|
]
|
||||||
|
|
||||||
|
initializeTaskType = (name) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.addListItem "taskTypes", name
|
||||||
|
persist.setItem "task:#{name}:running", 0
|
||||||
|
persist.setItem "task:#{name}:queued", 0
|
||||||
|
persist.setItem "task:#{name}:failed", 0
|
||||||
|
]
|
||||||
|
|
||||||
|
removeTaskType = (name) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.removeListItem "taskTypes", name
|
||||||
|
persist.removeItem "task:#{name}:running"
|
||||||
|
persist.removeItem "task:#{name}:queued"
|
||||||
|
persist.removeItem "task:#{name}:failed"
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
Promise.all [
|
||||||
|
initializeVariable "cdnRateLimit", "number", 0
|
||||||
|
initializeVariable "announcementText", "string", ""
|
||||||
|
initializeVariable "announcementLinkText", "string", ""
|
||||||
|
initializeVariable "announcementLink", "string", ""
|
||||||
|
initializeVariable "announcementVisible", "boolean", false
|
||||||
|
initializeVariable "maintenanceMode", "boolean", false
|
||||||
|
initializeVariable "maintenanceModeText", "text", ""
|
||||||
|
initializeTaskType "mirror"
|
||||||
|
initializeTaskType "thumbnail"
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
Promise.all [
|
||||||
|
removeVariable "cdnRateLimit"
|
||||||
|
removeVariable "announcementText"
|
||||||
|
removeVariable "announcementLinkText"
|
||||||
|
removeVariable "announcementLink"
|
||||||
|
removeVariable "announcementVisible"
|
||||||
|
removeVariable "maintenanceMode"
|
||||||
|
removeVariable "maintenanceModeText"
|
||||||
|
removeTaskType "mirror"
|
||||||
|
removeTaskType "thumbnail"
|
||||||
|
]
|
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.boolean "Disabled"
|
||||||
|
.defaultTo 0
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.dropColumn "Disabled"
|
@ -0,0 +1,24 @@
|
|||||||
|
rfr = require "rfr"
|
||||||
|
persist = rfr "lib/persist"
|
||||||
|
|
||||||
|
migrateTaskType = (name) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.setItem "task:#{name}:completed", 0
|
||||||
|
]
|
||||||
|
|
||||||
|
rollbackTaskType = (name) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.removeItem "task:#{name}:completed"
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
Promise.all [
|
||||||
|
migrateTaskType "mirror"
|
||||||
|
migrateTaskType "thumbnail"
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
Promise.all [
|
||||||
|
rollbackTaskType "mirror"
|
||||||
|
rollbackTaskType "thumbnail"
|
||||||
|
]
|
@ -0,0 +1,33 @@
|
|||||||
|
rfr = require "rfr"
|
||||||
|
persist = rfr "lib/persist"
|
||||||
|
|
||||||
|
initializeVariable = (name, type, initialValue) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.addListItem "variableTypes",
|
||||||
|
name: name
|
||||||
|
type: type
|
||||||
|
|
||||||
|
persist.setItem "var:#{name}", initialValue
|
||||||
|
]
|
||||||
|
|
||||||
|
removeVariable = (name) ->
|
||||||
|
Promise.all [
|
||||||
|
persist.removeListItemByFilter "variableTypes", (item) ->
|
||||||
|
return (item.name == name)
|
||||||
|
|
||||||
|
persist.removeItem "var:#{name}"
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
Promise.all [
|
||||||
|
initializeVariable "donationGoal", "number", 500
|
||||||
|
initializeVariable "donationTotal", "number", 0
|
||||||
|
initializeVariable "showNotice", "boolean", false
|
||||||
|
]
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
Promise.all [
|
||||||
|
removeVariable "donationGoal"
|
||||||
|
removeVariable "donationTotal"
|
||||||
|
removeVariable "showNotice"
|
||||||
|
]
|
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
exports.up = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.string "DisabledReason"
|
||||||
|
.nullable()
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) ->
|
||||||
|
knex.schema.table "documents", (table) ->
|
||||||
|
table.dropColumn "DisabledReason"
|
@ -0,0 +1,6 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
|
||||||
|
module.exports = (shelf) ->
|
||||||
|
shelf.model "BlogPost",
|
||||||
|
tableName: "blog_posts"
|
||||||
|
idAttribute: "Id"
|
@ -0,0 +1,6 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
|
||||||
|
module.exports = (shelf) ->
|
||||||
|
shelf.model "Document",
|
||||||
|
tableName: "documents"
|
||||||
|
idAttribute: "Id"
|
@ -0,0 +1,13 @@
|
|||||||
|
Promise = require "bluebird"
|
||||||
|
glob = Promise.promisify(require "glob")
|
||||||
|
rfr = require "rfr"
|
||||||
|
|
||||||
|
# This file automatically loads all models.
|
||||||
|
|
||||||
|
module.exports = (shelf) ->
|
||||||
|
Promise.try ->
|
||||||
|
glob "models/**/*.coffee"
|
||||||
|
.then (items) ->
|
||||||
|
for item in items
|
||||||
|
if item != "models/index.coffee"
|
||||||
|
rfr(item)(shelf)
|
@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"name": "pdfy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./bin/www"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"Base64": "^0.3.0",
|
||||||
|
"ansi-to-html": "^0.3.0",
|
||||||
|
"bhttp": "^1.0.3",
|
||||||
|
"bip21": "1.0.0",
|
||||||
|
"bluebird": "^2.9.3",
|
||||||
|
"body-parser": "~1.10.2",
|
||||||
|
"buffertools": "^2.1.2",
|
||||||
|
"chokidar": "^1.0.1",
|
||||||
|
"concat-stream": "^1.4.7",
|
||||||
|
"connect-busboy": "0.0.2",
|
||||||
|
"cookie-parser": "~1.3.3",
|
||||||
|
"csurf": "^1.8.0",
|
||||||
|
"debug": "~2.1.1",
|
||||||
|
"errors": "^0.2.0",
|
||||||
|
"express": "~4.11.1",
|
||||||
|
"express-brute": "^0.5.3",
|
||||||
|
"express-domain-middleware": "^0.1.0",
|
||||||
|
"express-promise-router": "0.0.7",
|
||||||
|
"express-session": "^1.11.1",
|
||||||
|
"file-stream-rotator": "joepie91/file-stream-rotator",
|
||||||
|
"glob": "^4.3.5",
|
||||||
|
"gm": "^1.17.0",
|
||||||
|
"ia-headers": "^1.0.0",
|
||||||
|
"jade": "~1.9.1",
|
||||||
|
"knex": "^0.7.6",
|
||||||
|
"lodash": "^3.3.1",
|
||||||
|
"marked": "^0.3.3",
|
||||||
|
"moment": "^2.9.0",
|
||||||
|
"morgan": "~1.5.1",
|
||||||
|
"mysql2": "^0.15.4",
|
||||||
|
"node-persist": "joepie91/node-persist",
|
||||||
|
"nodemailer": "^1.3.4",
|
||||||
|
"pretty-error": "^1.1.1",
|
||||||
|
"qr-image": "^3.1.0",
|
||||||
|
"read": "^1.0.5",
|
||||||
|
"rfr": "^1.2.2",
|
||||||
|
"scrypt-for-humans": "^1.0.1",
|
||||||
|
"serve-favicon": "~2.2.0",
|
||||||
|
"session-file-store": "0.0.3",
|
||||||
|
"slug": "^0.8.0",
|
||||||
|
"stream-length": "^1.0.2",
|
||||||
|
"uuid": "^2.0.1",
|
||||||
|
"xtend": "^4.0.0",
|
||||||
|
"zero-fill": "^2.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autosize": "^3.0.0",
|
||||||
|
"blueimp-file-upload": "^9.9.3",
|
||||||
|
"coffee-loader": "^0.7.2",
|
||||||
|
"coffee-script": "^1.9.1",
|
||||||
|
"gulp": "^3.8.10",
|
||||||
|
"gulp-cached": "~0.0.3",
|
||||||
|
"gulp-coffee": "~2.0.1",
|
||||||
|
"gulp-concat": "~2.2.0",
|
||||||
|
"gulp-jade": "^0.7.0",
|
||||||
|
"gulp-livereload": "~2.1.0",
|
||||||
|
"gulp-nodemon": "~1.0.4",
|
||||||
|
"gulp-notify": "^1.6.0",
|
||||||
|
"gulp-plumber": "~0.6.3",
|
||||||
|
"gulp-remember": "~0.2.0",
|
||||||
|
"gulp-rename": "~1.2.0",
|
||||||
|
"gulp-sass": "^1.3.3",
|
||||||
|
"gulp-util": "~2.2.17",
|
||||||
|
"gulp-webpack": "^1.3.0",
|
||||||
|
"imports-loader": "^0.6.3",
|
||||||
|
"jquery": "^2.1.3",
|
||||||
|
"pretty-units": "^0.1.0"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,714 @@
|
|||||||
|
@-webkit-keyframes pulsate {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 0px #000000; }
|
||||||
|
|
||||||
|
40% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
60% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0px #000000; } }
|
||||||
|
|
||||||
|
@-moz-keyframes pulsate {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 0px #000000; }
|
||||||
|
|
||||||
|
40% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
60% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0px #000000; } }
|
||||||
|
|
||||||
|
@-o-keyframes pulsate {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 0px #000000; }
|
||||||
|
|
||||||
|
40% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
60% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0px #000000; } }
|
||||||
|
|
||||||
|
@-ms-keyframes pulsate {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 0px #000000; }
|
||||||
|
|
||||||
|
40% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
60% {
|
||||||
|
box-shadow: 0 0 15px #005f52; }
|
||||||
|
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0px #000000; } }
|
||||||
|
|
||||||
|
.pulsate {
|
||||||
|
-webkit-animation-name: pulsate;
|
||||||
|
-webkit-animation-duration: 800ms;
|
||||||
|
-webkit-animation-iteration-count: 3;
|
||||||
|
-moz-animation-name: pulsate;
|
||||||
|
-moz-animation-duration: 800ms;
|
||||||
|
-moz-animation-iteration-count: 3;
|
||||||
|
-o-animation-name: pulsate;
|
||||||
|
-o-animation-duration: 800ms;
|
||||||
|
-o-animation-iteration-count: 3;
|
||||||
|
-ms-animation-name: pulsate;
|
||||||
|
-ms-animation-duration: 800ms;
|
||||||
|
-ms-animation-iteration-count: 3; }
|
||||||
|
|
||||||
|
.error-stack {
|
||||||
|
background-color: black;
|
||||||
|
padding: 16px; }
|
||||||
|
|
||||||
|
.side-margins {
|
||||||
|
margin-left: 64px;
|
||||||
|
margin-right: 64px; }
|
||||||
|
|
||||||
|
.pure-button-small {
|
||||||
|
padding: 6px 11px;
|
||||||
|
font-size: 15px; }
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both; }
|
||||||
|
|
||||||
|
.return-button {
|
||||||
|
margin-top: -3px;
|
||||||
|
float: right; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #2D2D31;
|
||||||
|
color: white;
|
||||||
|
font-family: "PT Sans", sans-serif;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
font-size: 16px; }
|
||||||
|
body.full-screen {
|
||||||
|
overflow: hidden; }
|
||||||
|
body a {
|
||||||
|
color: #94EBD9;
|
||||||
|
text-decoration: none; }
|
||||||
|
body a:hover {
|
||||||
|
/*color: #67C7B3;*/
|
||||||
|
color: #BAF5E9; }
|
||||||
|
body section, body .popup {
|
||||||
|
padding: 16px 19px;
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 24px; }
|
||||||
|
body section h3, body .popup h3 {
|
||||||
|
margin-top: 0px; }
|
||||||
|
body .popup {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 0px;
|
||||||
|
display: inline-block; }
|
||||||
|
body .popup label, body .popup input, body .popup button {
|
||||||
|
margin-right: 12px;
|
||||||
|
padding: 0.2em 0.6em !important; }
|
||||||
|
body .popup input {
|
||||||
|
width: 400px; }
|
||||||
|
body .pure-button {
|
||||||
|
background-color: #000000;
|
||||||
|
border: 1px solid #0B7474;
|
||||||
|
color: white; }
|
||||||
|
body .pure-button.inline {
|
||||||
|
padding: 3px 9px;
|
||||||
|
margin: 0px 4px; }
|
||||||
|
body .wrapper {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0px auto; }
|
||||||
|
body .header, body .contents, body .subtext {
|
||||||
|
padding: 18px; }
|
||||||
|
body .subtext {
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
padding: 4px 18px; }
|
||||||
|
body .progress-bar {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 16px 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #06282D;
|
||||||
|
padding: 4px 3px;
|
||||||
|
height: 24px; }
|
||||||
|
body .progress-fill {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #0a8071;
|
||||||
|
height: 24px; }
|
||||||
|
body .progress-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
text-shadow: 0px 0px 3px #000000;
|
||||||
|
-webkit-text-shadow: 0px 0px 3px #000000;
|
||||||
|
-moz-text-shadow: 0px 0px 3px #000000;
|
||||||
|
-o-text-shadow: 0px 0px 3px #000000;
|
||||||
|
-ms-text-shadow: 0px 0px 3px #000000; }
|
||||||
|
body .progress-container {
|
||||||
|
position: relative;
|
||||||
|
height: 24px;
|
||||||
|
margin: 28px 0px; }
|
||||||
|
body .progress-container label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 4px; }
|
||||||
|
body .progress-container .progress-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 250px;
|
||||||
|
right: 0px;
|
||||||
|
margin: 0px; }
|
||||||
|
body .progress-container .progress-text {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 16px; }
|
||||||
|
body .header {
|
||||||
|
background-color: black;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 42px; }
|
||||||
|
body .header h1 {
|
||||||
|
margin: 0px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 32px; }
|
||||||
|
body .header h1 a {
|
||||||
|
text-decoration: inherit;
|
||||||
|
color: inherit; }
|
||||||
|
body .header .pure-button {
|
||||||
|
background-color: #3C3C3C; }
|
||||||
|
body .header .button-upload, body .header .button-lite {
|
||||||
|
/*display: inline-block;
|
||||||
|
margin-left: 32px;
|
||||||
|
vertical-align: 5px;*/
|
||||||
|
float: right; }
|
||||||
|
body .header .button-lite {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: none;
|
||||||
|
margin-right: 4px; }
|
||||||
|
body .header .button-lite:hover {
|
||||||
|
background-color: #3C3C3C;
|
||||||
|
border: 1px solid #0B7474; }
|
||||||
|
body .header .abuse {
|
||||||
|
float: right;
|
||||||
|
margin-right: 64px;
|
||||||
|
margin-top: 10px; }
|
||||||
|
body .contents h2 {
|
||||||
|
margin-top: 0px; }
|
||||||
|
body .contents #upload_activator {
|
||||||
|
margin-bottom: 18px; }
|
||||||
|
body .contents .latest {
|
||||||
|
margin-top: 64px; }
|
||||||
|
body .contents .latest a.document {
|
||||||
|
display: inline-block;
|
||||||
|
width: 150px;
|
||||||
|
height: 218px;
|
||||||
|
background-color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 0px;
|
||||||
|
margin-right: 6px; }
|
||||||
|
body .contents .latest a.document img {
|
||||||
|
width: 150px; }
|
||||||
|
body .contents .latest a.gallery-link {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
padding: 12px 64px; }
|
||||||
|
body .contents .upload-form {
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 38px; }
|
||||||
|
body .contents .upload-form .button-browse {
|
||||||
|
font-size: 24px;
|
||||||
|
vertical-align: -1px;
|
||||||
|
margin-right: 24px; }
|
||||||
|
body .contents .upload-form .faded {
|
||||||
|
opacity: 0.4; }
|
||||||
|
body .contents .upload-form .info, body .contents .upload-form .fileinfo, body .contents .upload-form .progress {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 64px auto 0px auto; }
|
||||||
|
body .contents .upload-form .fileinfo {
|
||||||
|
display: none; }
|
||||||
|
body .contents .upload-form .fileinfo h2 {
|
||||||
|
margin: 0px 0px 16px 0px; }
|
||||||
|
body .contents .upload-form .fileinfo label {
|
||||||
|
margin-left: 7px; }
|
||||||
|
body .contents .upload-form .fileinfo .button-submit {
|
||||||
|
float: right;
|
||||||
|
margin-top: 16px; }
|
||||||
|
body .contents .upload-form .progress {
|
||||||
|
display: none;
|
||||||
|
margin-top: 32px; }
|
||||||
|
body .contents .upload-form .progress .wait {
|
||||||
|
display: none; }
|
||||||
|
body .contents .upload-form #uploadError {
|
||||||
|
background-color: #CD1E32;
|
||||||
|
padding: 20px 28px;
|
||||||
|
text-align: left;
|
||||||
|
width: 700px;
|
||||||
|
margin: 0px auto;
|
||||||
|
display: none;
|
||||||
|
margin-top: 16px; }
|
||||||
|
body .contents .upload-form #uploadError h3 {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 7px; }
|
||||||
|
body .contents .upload-form #uploadError p {
|
||||||
|
margin: 0px; }
|
||||||
|
body .contents .upload-form #upload_element {
|
||||||
|
display: none; }
|
||||||
|
body .contents .upload-form .bar, body .contents .upload-form .bar-inner {
|
||||||
|
height: 12px; }
|
||||||
|
body .contents .upload-form .bar {
|
||||||
|
border: 1px solid #0B7474;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 8px; }
|
||||||
|
body .contents .upload-form .bar-inner {
|
||||||
|
background-color: #014949;
|
||||||
|
width: 0%;
|
||||||
|
border-radius: 3px; }
|
||||||
|
body .viewer-contents .embed_code, body .viewer-contents .link_code {
|
||||||
|
background-color: #1C1C1C;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
color: #E1E1E1; }
|
||||||
|
body .viewer-contents textarea.embed_code, body .viewer-contents .link_code {
|
||||||
|
width: 247px;
|
||||||
|
margin-top: 7px; }
|
||||||
|
body .viewer-contents input.embed_code {
|
||||||
|
width: 275px; }
|
||||||
|
body .viewer-contents .viewer-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 78px;
|
||||||
|
/*top: 117px;*/
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 320px;
|
||||||
|
overflow: hidden; }
|
||||||
|
body .viewer-contents .viewer-wrapper .viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0px; }
|
||||||
|
body .viewer-contents .bottombar {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 8px; }
|
||||||
|
body .viewer-contents .bottombar .header-wrapper {
|
||||||
|
width: 99%; }
|
||||||
|
body .viewer-contents .bottombar .header-wrapper h2 {
|
||||||
|
display: block;
|
||||||
|
margin: 0px 0px 3px 0px;
|
||||||
|
font-size: 17px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; }
|
||||||
|
body .viewer-contents .bottombar .tools {
|
||||||
|
font-size: 14px; }
|
||||||
|
body .viewer-contents .bottombar .tools .views {
|
||||||
|
float: left;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: bold; }
|
||||||
|
body .viewer-contents .bottombar .tools .embed {
|
||||||
|
float: right;
|
||||||
|
margin-right: 48px; }
|
||||||
|
body .viewer-contents .bottombar .tools .download {
|
||||||
|
float: right; }
|
||||||
|
body .viewer-contents .sidebar {
|
||||||
|
position: absolute;
|
||||||
|
top: 78px;
|
||||||
|
/*top: 117px;*/
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
width: 320px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
overflow-y: auto; }
|
||||||
|
body .viewer-contents .sidebar .actual-contents {
|
||||||
|
padding: 16px; }
|
||||||
|
body .viewer-contents .sidebar h2 {
|
||||||
|
margin: 6px 0px 0px 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; }
|
||||||
|
body .viewer-contents .sidebar .embed_code {
|
||||||
|
height: 100px; }
|
||||||
|
body .viewer-contents .sidebar .download-box, body .viewer-contents .sidebar .embed-box, body .viewer-contents .sidebar .link-box {
|
||||||
|
padding: 13px 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border-radius: 4px; }
|
||||||
|
body .viewer-contents .sidebar .download-box {
|
||||||
|
background-color: #0b4d56; }
|
||||||
|
body .viewer-contents .sidebar .download-box .formats {
|
||||||
|
margin-top: 12px; }
|
||||||
|
body .viewer-contents .sidebar .embed-box, body .viewer-contents .sidebar .link-box {
|
||||||
|
background-color: #3a3e45; }
|
||||||
|
body .viewer-contents .sidebar h3 {
|
||||||
|
margin: 0px 0px 4px 0px; }
|
||||||
|
body .viewer-contents .sidebar p {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 0px; }
|
||||||
|
body .viewer-contents .sidebar .donation-box {
|
||||||
|
padding: 0px 16px 16px 16px;
|
||||||
|
background-color: black;
|
||||||
|
text-align: center; }
|
||||||
|
body .viewer-contents .sidebar .donation-box h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px; }
|
||||||
|
body .viewer-contents .sidebar .donation-box p.amounts {
|
||||||
|
font-size: 21px;
|
||||||
|
margin-bottom: 12px; }
|
||||||
|
body .viewer-contents .sidebar .donation-box .donation-buttons {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 15px; }
|
||||||
|
body .viewer-contents .sidebar .donation-box .donation-buttons a.pure-button {
|
||||||
|
margin-right: 16px;
|
||||||
|
padding: .4em .9em; }
|
||||||
|
body .viewer-contents .sidebar .toolbar-settings {
|
||||||
|
margin: 8px 3px; }
|
||||||
|
body .viewer-contents .sidebar .toolbar-settings input {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
margin-right: 6px; }
|
||||||
|
body .viewer-contents .sidebar .toolbar-settings label {
|
||||||
|
margin-right: 14px;
|
||||||
|
margin-top: -2px;
|
||||||
|
font-size: 14px; }
|
||||||
|
body .viewer-contents .sidebar .stats {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
text-align: center; }
|
||||||
|
body.announcement-visible .viewer-contents .viewer-wrapper, body.announcement-visible .viewer-contents .sidebar {
|
||||||
|
top: 117px; }
|
||||||
|
body .gallery {
|
||||||
|
background-color: #242427;
|
||||||
|
border: 1px solid black; }
|
||||||
|
body .gallery .next, body .gallery .previous {
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px; }
|
||||||
|
body .gallery .next {
|
||||||
|
float: right; }
|
||||||
|
body .gallery .previous {
|
||||||
|
float: left; }
|
||||||
|
body .gallery .document {
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
display: block;
|
||||||
|
color: white; }
|
||||||
|
body .gallery .document:after {
|
||||||
|
content: ".";
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
clear: both;
|
||||||
|
visibility: hidden; }
|
||||||
|
body .gallery .document:hover {
|
||||||
|
background-color: #2a2a2e; }
|
||||||
|
body .gallery .document span {
|
||||||
|
display: block; }
|
||||||
|
body .gallery .document span.thumb {
|
||||||
|
width: 83px;
|
||||||
|
height: 120px;
|
||||||
|
float: left;
|
||||||
|
background-color: white;
|
||||||
|
margin-right: 16px; }
|
||||||
|
body .gallery .document span.thumb img {
|
||||||
|
height: 120px; }
|
||||||
|
body .gallery .document .name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 24px; }
|
||||||
|
body .gallery .document .date {
|
||||||
|
font-size: 20px; }
|
||||||
|
body .gallery .document .views {
|
||||||
|
font-size: 18px; }
|
||||||
|
body .donation-page .js-available {
|
||||||
|
display: none; }
|
||||||
|
body .donation-page .bip21-qr {
|
||||||
|
float: right;
|
||||||
|
margin: -25px 12px 12px 12px; }
|
||||||
|
body .donation-page section:after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both; }
|
||||||
|
body .donation-page section.instructions {
|
||||||
|
min-height: 200px; }
|
||||||
|
body .donation-page .option {
|
||||||
|
float: left;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin: 5px;
|
||||||
|
width: 190px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
position: relative;
|
||||||
|
background-color: #0D0D0D; }
|
||||||
|
body .donation-page .option.payment-method {
|
||||||
|
height: 75px; }
|
||||||
|
body .donation-page .option.payment-method label {
|
||||||
|
font-size: 14px; }
|
||||||
|
body .donation-page .option.payment-method label.fixed {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 21px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
text-align: center; }
|
||||||
|
body .donation-page .option.payment-method label.logo {
|
||||||
|
font-size: 33px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #efefef; }
|
||||||
|
body .donation-page .option.selected {
|
||||||
|
background-color: #003D35; }
|
||||||
|
body .donation-page .option label, body .donation-page .option input {
|
||||||
|
display: block;
|
||||||
|
margin: 0px auto; }
|
||||||
|
body .donation-page .option label {
|
||||||
|
margin-bottom: 9px; }
|
||||||
|
body .donation-page .option input[type="radio"] {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px; }
|
||||||
|
body .donation-page .option .exchange-rate {
|
||||||
|
font-size: 15px; }
|
||||||
|
body .donation-page .option #custom_amount_input {
|
||||||
|
width: 72px;
|
||||||
|
display: inline;
|
||||||
|
margin-left: 5px;
|
||||||
|
background-color: #202020;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 4px 3px 4px;
|
||||||
|
font-size: 19px; }
|
||||||
|
body .donation-page .paypal-button {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #0D0D0D; }
|
||||||
|
body .donation-page .paypal-button:hover {
|
||||||
|
background-color: black; }
|
||||||
|
body .blog-post h2, body .blog-index h2 {
|
||||||
|
font-size: 29px; }
|
||||||
|
body .blog-index .post {
|
||||||
|
margin-bottom: 9px; }
|
||||||
|
body .blog-index .date {
|
||||||
|
font-family: monospace;
|
||||||
|
margin-right: 16px;
|
||||||
|
color: #dbdbdb;
|
||||||
|
font-size: 15px; }
|
||||||
|
body .blog-index .title {
|
||||||
|
margin-left: 8px; }
|
||||||
|
body .blog-post section {
|
||||||
|
padding: 1px 24px; }
|
||||||
|
body .blog-post a.anchor {
|
||||||
|
float: left;
|
||||||
|
margin-left: -16px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 80%; }
|
||||||
|
body .blog-post h3 {
|
||||||
|
font-size: 24px; }
|
||||||
|
body .blog-post h4 {
|
||||||
|
font-size: 19px; }
|
||||||
|
body .admin {
|
||||||
|
position: relative; }
|
||||||
|
body .admin th, body .admin td {
|
||||||
|
padding: 4px 7px; }
|
||||||
|
body .admin th {
|
||||||
|
text-align: left; }
|
||||||
|
body .admin td {
|
||||||
|
border-top: 1px solid gray; }
|
||||||
|
body .admin .save-button {
|
||||||
|
margin-right: 8px; }
|
||||||
|
body .admin form.pure-g [class*="pure-u"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 5px; }
|
||||||
|
body .admin form.pure-g label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px; }
|
||||||
|
body .admin form.pure-g .md-editor {
|
||||||
|
font-family: monospace; }
|
||||||
|
body .admin form.pure-g .md-preview {
|
||||||
|
border: 1px solid #575757;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px; }
|
||||||
|
body .admin form.pure-g .submit {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
margin-top: -6px; }
|
||||||
|
body .error h2 {
|
||||||
|
font-size: 22px; }
|
||||||
|
body .pure-button img.icon {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
margin-right: 6px; }
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #DEDEDE;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #DEDEDE; }
|
||||||
|
|
||||||
|
#drag_ghost {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #1D3030;
|
||||||
|
border: 1px solid #0B7474;
|
||||||
|
box-shadow: 3px 3px 8px 0px #131314;
|
||||||
|
color: white;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 5px; }
|
||||||
|
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.button-lite.hide-x-small {
|
||||||
|
display: none; } }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.bottombar .embed {
|
||||||
|
display: none; } }
|
||||||
|
|
||||||
|
@media (max-width: 660px) {
|
||||||
|
.alt-small {
|
||||||
|
display: block; }
|
||||||
|
.alt-large {
|
||||||
|
display: none; } }
|
||||||
|
|
||||||
|
@media (min-width: 650px) {
|
||||||
|
.alt-small {
|
||||||
|
display: none; }
|
||||||
|
.alt-large {
|
||||||
|
display: block; } }
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.dragdrop-instructions {
|
||||||
|
display: block; } }
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.hide-small {
|
||||||
|
display: none; } }
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
body .viewer-contents .sidebar {
|
||||||
|
display: none; }
|
||||||
|
body .viewer-contents .viewer-wrapper {
|
||||||
|
right: 0px;
|
||||||
|
bottom: 64px; }
|
||||||
|
body .viewer-contents .bottombar {
|
||||||
|
display: block; } }
|
||||||
|
|
||||||
|
@media (max-height: 940px) {
|
||||||
|
body .header {
|
||||||
|
padding: 10px 18px; }
|
||||||
|
body .viewer-contents .viewer-wrapper, body .viewer-contents .sidebar {
|
||||||
|
top: 62px; }
|
||||||
|
body .viewer-contents .sidebar h2 {
|
||||||
|
margin: 0px;
|
||||||
|
font-size: 20px; }
|
||||||
|
body .viewer-contents .sidebar h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0px; }
|
||||||
|
body .viewer-contents .sidebar p {
|
||||||
|
margin: 1px 0px; }
|
||||||
|
body .viewer-contents .sidebar .embed-box, body .viewer-contents .sidebar .link-box, body .viewer-contents .sidebar .download-box {
|
||||||
|
margin: 16px 0px;
|
||||||
|
padding: 8px 16px 10px 16px; }
|
||||||
|
body .viewer-contents .sidebar .donation-box {
|
||||||
|
padding-top: 8px; }
|
||||||
|
body .viewer-contents .sidebar textarea.embed_code {
|
||||||
|
height: 32px; }
|
||||||
|
body .viewer-contents .sidebar .stats {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0px 8px; }
|
||||||
|
body.announcement-visible .viewer-contents .viewer-wrapper, body.announcement-visible .viewer-contents .sidebar {
|
||||||
|
top: 101px; } }
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
background-color: #086458;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 14px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
border-radius: 4px; }
|
||||||
|
.notice a {
|
||||||
|
color: white; }
|
||||||
|
.notice p {
|
||||||
|
margin: 8px 0px; }
|
||||||
|
|
||||||
|
.announce {
|
||||||
|
background-color: #0A8071;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 18px; }
|
||||||
|
.announce a {
|
||||||
|
color: white; }
|
||||||
|
|
||||||
|
.full-screen .announce {
|
||||||
|
height: 21px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 17px;
|
||||||
|
padding: 9px 16px; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
background-color: white; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: #5A5A61; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #171717; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:vertical {
|
||||||
|
border-left: 1px solid #323235; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb-vertical {
|
||||||
|
border-left: 1px solid #323235; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track:horizontal {
|
||||||
|
border-top: 1px solid #323235; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb-horizontal {
|
||||||
|
border-top: 1px solid #323235; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
background-color: #1C1C1C;
|
||||||
|
color: white;
|
||||||
|
background-position: 0px -1px; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:vertical:increment {
|
||||||
|
background-image: url(/static/pdfjs/images/arrow-down.png);
|
||||||
|
border-top: 1px solid black; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:vertical:decrement {
|
||||||
|
background-image: url(/static/pdfjs/images/arrow-up.png);
|
||||||
|
border-bottom: 1px solid black; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:horizontal:increment {
|
||||||
|
background-image: url(/static/pdfjs/images/arrow-right.png);
|
||||||
|
border-left: 1px solid black; }
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button:horizontal:decrement {
|
||||||
|
background-image: url(/static/pdfjs/images/arrow-left.png);
|
||||||
|
border-right: 1px solid black; }
|
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 3.4 KiB |