commit
052944aa01
29 changed files with 2588 additions and 0 deletions
Split View
Diff Options
-
2.gitignore
-
85config.js
-
25index.js
-
1328package-lock.json
-
32package.json
-
129server/booth.js
-
56server/index.js
-
5server/public/css/bootstrap.min.css
-
163server/public/css/cover.css
-
8server/public/css/style.css
-
10server/public/css/view.css
-
7server/public/js/bootstrap.min.js
-
4server/public/js/jquery-1.11.2.min.js
-
3server/public/js/socket.io-1.3.4.js
-
8server/public/js/socket.io.js
-
1server/public/js/socket.io.js.map
-
BINserver/public/rsc/common/294.GIF
-
BINserver/public/rsc/footer/cloclo.jpg
-
BINserver/public/rsc/footer/m-30ansv2.jpg
-
BINserver/public/rsc/footer/m-cloclo.jpg
-
BINserver/public/rsc/footer/m-clotilde60.jpg
-
BINserver/public/rsc/template/default.jpg
-
BINserver/public/waiting.gif
-
BINserver/public/waiting2.gif
-
172server/views/index.ejs
-
226server/views/scope.ejs
-
62server/views/test.ejs
-
107tools/tools-gphoto2.js
-
155tools/tools-photobooth.js
@ -0,0 +1,2 @@ |
|||
*node_modules* |
|||
output/ |
|||
@ -0,0 +1,85 @@ |
|||
const os = require('os') |
|||
const path = require('path') |
|||
const mkdirp = require('mkdirp') |
|||
|
|||
const config = { |
|||
background: '#ffffff', |
|||
margins: 0.005, |
|||
style: 'default', |
|||
nbPicture: 4, |
|||
cropSize: { |
|||
width: 900, |
|||
height: 1200 |
|||
}, |
|||
pictNames: [], |
|||
layout: [], |
|||
booths: {}, |
|||
paths: { |
|||
final: '', |
|||
template: '', |
|||
prebuilt: '', |
|||
original: '', |
|||
toprint: '' |
|||
}, |
|||
footer: 'm-30ansv2.jpg' |
|||
} |
|||
|
|||
config.server = { |
|||
ipAddress: getIpAddress(), |
|||
port: 3111 |
|||
} |
|||
|
|||
config.killZone = { |
|||
top: 2, |
|||
bottom: 2, |
|||
left: 25, |
|||
right: 25 |
|||
} |
|||
|
|||
// Init PATH
|
|||
config.paths.template = path.resolve(__dirname, './server/public/rsc/template') |
|||
config.footer = path.resolve(__dirname, './server/public/rsc/footer', config.footer) |
|||
config.paths.final = path.resolve(__dirname, './output/final') |
|||
config.paths.final_ld = path.resolve(__dirname, './output/final_ld') |
|||
config.paths.prebuilt = path.resolve(__dirname, './output/cache') |
|||
config.paths.original = path.resolve(__dirname, './output/original') |
|||
config.paths.toprint = path.resolve(__dirname, './output/toprint') |
|||
|
|||
mkdirp.sync(config.paths.final) |
|||
mkdirp.sync(config.paths.final_ld) |
|||
mkdirp.sync(config.paths.prebuilt) |
|||
mkdirp.sync(config.paths.original) |
|||
mkdirp.sync(config.paths.toprint) |
|||
|
|||
// Init pinctures names
|
|||
for (let index = 0; index < config.nbPicture; index++) { |
|||
config.pictNames.push('pict_' + index + '.jpg') |
|||
} |
|||
|
|||
module.exports = config |
|||
|
|||
function getIpAddress () { |
|||
let ipAddress = 'localhost' |
|||
// Get local server ip address
|
|||
let ifaces = os.networkInterfaces() |
|||
Object.keys(ifaces).forEach(function (ifname) { |
|||
let alias = 0 |
|||
|
|||
ifaces[ifname].forEach(function (iface) { |
|||
// skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
|
|||
if (iface.family !== 'IPv4' || iface.internal !== false) return |
|||
if (/wlan/i.test(ifname)) ipAddress = iface.address |
|||
|
|||
if (alias >= 1) { |
|||
// this single interface has multiple ipv4 addresses
|
|||
// console.log(`[-] network: ${ifname} : ${alias} - ${iface.address}`)
|
|||
} else { |
|||
// this interface has only one ipv4 adress
|
|||
// console.log(`[-] network: ${ifname} - ${iface.address}`)
|
|||
// console.log(ifname, iface.address)
|
|||
} |
|||
}) |
|||
}) |
|||
|
|||
return ipAddress |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
const gphoto2 = require('./tools/tools-gphoto2') |
|||
const server = require('./server') |
|||
|
|||
// gphoto2.init(function (pErr) {
|
|||
// console.log('DONE')
|
|||
let app = server() |
|||
// })
|
|||
|
|||
// // Initialize the library
|
|||
// BOB_var_gamepad.init()
|
|||
// // List the state of all currently attached devices
|
|||
// for (var i = 0, l = BOB_var_gamepad.numDevices(); i < l; i++) {
|
|||
// console.log(i, BOB_var_gamepad.deviceAtIndex(i));
|
|||
// }
|
|||
// // Create a game loop and poll for events
|
|||
// setInterval(BOB_var_gamepad.processEvents, 16);
|
|||
// // Scan for new gamepads as a slower rate
|
|||
// setInterval(BOB_var_gamepad.detectDevices, 500);
|
|||
// // Listen for button down events on all gamepads
|
|||
// BOB_var_gamepad.on("down", function (pId, pNum) {
|
|||
// BOB_cfg_config.io.emit('boothClick', {
|
|||
// id : pId,
|
|||
// num : pNum
|
|||
// });
|
|||
// })
|
|||
1328
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,32 @@ |
|||
{ |
|||
"name": "bobinoscope-v2", |
|||
"version": "1.0.0", |
|||
"description": "Code for bobinoscope", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"author": "P.BARRY", |
|||
"license": "ISC", |
|||
"dependencies": { |
|||
"async": "^2.6.1", |
|||
"body-parser": "^1.18.3", |
|||
"colors": "^1.3.1", |
|||
"cookie-parser": "^1.4.3", |
|||
"ejs": "^2.6.1", |
|||
"express": "^4.16.3", |
|||
"fs-extra": "^7.0.0", |
|||
"gamepad": "^1.6.0", |
|||
"gm": "^1.23.1", |
|||
"mkdirp": "^0.5.1", |
|||
"morgan": "^1.9.0", |
|||
"opn": "^5.3.0", |
|||
"socket.io": "^2.1.1", |
|||
"split-lines": "^2.0.0" |
|||
}, |
|||
"nodemonConfig": { |
|||
"ignore": [ |
|||
"webapp/*" |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
const express = require('express') |
|||
const router = express.Router() |
|||
const fs = require('fs-extra') |
|||
const gm = require('gm').subClass({ imageMagick: true }) |
|||
const path = require('path') |
|||
const gamepad = require('gamepad') |
|||
require('colors') |
|||
|
|||
const gphoto2 = require('../tools/tools-gphoto2') |
|||
const booth = require('../tools/tools-photobooth') |
|||
|
|||
const CFG = require('../config') |
|||
const CTX = { |
|||
__io: null, |
|||
io: function () { |
|||
return CTX.__io |
|||
}, |
|||
inProgress: 0, |
|||
testPath: '/tmp/bobinoscope-test.jpg' |
|||
} |
|||
|
|||
booth.init() |
|||
setGamepad() |
|||
|
|||
// router.use(function (req, res, next) {
|
|||
// console.log(req.url)
|
|||
// next()
|
|||
// })
|
|||
|
|||
// ////////////////////////////////////////////////////////////////////////////
|
|||
// DSLR
|
|||
router.get('/dslr/takepicture/:boothId/:pictId', function (req, res, next) { |
|||
// return res.json({})
|
|||
gphoto2.takeOnePicture(req.params.boothId, req.params.pictId, function (pErr) { |
|||
res.json({ error: pErr, data: null }) |
|||
}) |
|||
}) |
|||
|
|||
router.get('/build/:boothId', function (req, res, next) { |
|||
console.log('Building') |
|||
// res.json({ok: 1})
|
|||
CTX.inProgress += 1 |
|||
CTX.io().emit('boothState') |
|||
booth.buildBooth(req.params.boothId, function (pErr) { |
|||
CTX.io().emit('boothState') |
|||
CTX.inProgress -= 1 |
|||
res.json({ error: pErr, data: null, boothId: req.params.boothId }) |
|||
}) |
|||
}) |
|||
|
|||
router.use('/build/result/low', express.static(CFG.paths.final_ld)) |
|||
router.use('/build/result', express.static(CFG.paths.final)) |
|||
|
|||
// ////////////////////////////////////////////////////////////////////////////
|
|||
// PRINTER
|
|||
router.get('/list/:from', function (req, res, next) { |
|||
booth.getBoothList(req.params.from, function (pErr, pList) { |
|||
res.json({error: pErr, booths: pList, inProgress: CTX.inProgress}) |
|||
}) |
|||
}) |
|||
|
|||
router.get('/toprint', function (req, res, next) { |
|||
var todoPath = path.join(CFG.paths.toprint) |
|||
fs.readdir(todoPath, function (pErr, pList) { |
|||
res.json({error: pErr, todos: pList}) |
|||
}) |
|||
}) |
|||
|
|||
router.get('/print/:pictId', function (req, res, next) { |
|||
let srcPath = path.join(CFG.paths.final, req.params.pictId) |
|||
let dstPath = path.join(CFG.paths.toprint, req.params.pictId) |
|||
fs.copy(srcPath, dstPath, function (pErr) { |
|||
res.json({error: pErr}) |
|||
}) |
|||
}) |
|||
|
|||
// ////////////////////////////////////////////////////////////////////////////
|
|||
// TEST
|
|||
|
|||
router.get('/test/takepicture', function (req, res, next) { |
|||
let tmpPict = '/tmp/bobinoscope-test.tmp.jpg' |
|||
gphoto2.__takeOnePicture(tmpPict, function (pErr) { |
|||
if (pErr) return res.status(500).send(pErr) |
|||
gm(tmpPict) |
|||
.autoOrient() |
|||
.gravity('Center') |
|||
.write(CTX.testPath, function (pErr) { |
|||
if (pErr) console.log('Failed to resize picture', pErr) |
|||
res.json({ok: 1}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
router.get('/test/takepicture/result', function (req, res, next) { |
|||
res.sendFile('/tmp/bobinoscope-test.jpg') |
|||
}) |
|||
|
|||
router.setSocketIo = function (pIo) { |
|||
CTX.__io = pIo |
|||
} |
|||
|
|||
function setGamepad () { |
|||
// Initialize the library
|
|||
gamepad.init() |
|||
// List the state of all currently attached devices
|
|||
// for (var i = 0, l = gamepad.numDevices(); i < l; i++) {
|
|||
// console.log(i, gamepad.deviceAtIndex(i))
|
|||
// }
|
|||
|
|||
if (!gamepad.numDevices()) { |
|||
console.log('[!] No gamepad found'.yellow) |
|||
} else { |
|||
console.log(`[!] ${gamepad.numDevices()} gamepad(s) found`.bgBlue) |
|||
// Create a game loop and poll for events
|
|||
setInterval(gamepad.processEvents, 16) |
|||
// Scan for new gamepads as a slower rate
|
|||
setInterval(gamepad.detectDevices, 500) |
|||
// Listen for button down events on all gamepads
|
|||
gamepad.on('down', function (pId, pNum) { |
|||
console.log(`[!] gamepad action received | ${pId}:${pNum}`.bgBlue) |
|||
CTX.io().emit('boothClick', { |
|||
id: pId, |
|||
num: pNum |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,56 @@ |
|||
const path = require('path') |
|||
const express = require('express') |
|||
const app = express() |
|||
const logger = require('morgan') |
|||
const cookieParser = require('cookie-parser') |
|||
const bodyParser = require('body-parser') |
|||
|
|||
const booth = require('./booth') |
|||
|
|||
const CFG = require('../config') |
|||
const PKG = require('../package') |
|||
|
|||
// view engine setup
|
|||
app.set('views', path.join(__dirname, 'views')) |
|||
app.set('view engine', 'ejs') |
|||
app.use(logger('dev')) |
|||
app.use(bodyParser.json()) |
|||
app.use(bodyParser.urlencoded({ extended: false })) |
|||
app.use(cookieParser()) |
|||
app.use(express.static(path.join(__dirname, 'public'))) |
|||
|
|||
app.use('/booth', booth) |
|||
|
|||
app.get('/', (req, res) => { |
|||
res.render('index', { |
|||
serverIpAddress: CFG.server.ipAddress, |
|||
serverPort: CFG.server.port |
|||
}) |
|||
}) |
|||
|
|||
app.get('/test', (req, res) => { |
|||
res.render('test', { |
|||
serverIpAddress: CFG.server.ipAddress, |
|||
serverPort: CFG.server.port |
|||
}) |
|||
}) |
|||
|
|||
app.get('/bobinoscope', (req, res) => { |
|||
res.render('scope', { |
|||
serverIpAddress: CFG.server.ipAddress, |
|||
serverPort: CFG.server.port, |
|||
killZone: CFG.killZone, |
|||
renderingTime: 5000 |
|||
}) |
|||
}) |
|||
|
|||
module.exports = function () { |
|||
let server = app.listen(CFG.server.port, () => { |
|||
console.log(`[-] Server running on: ${CFG.server.ipAddress}:${CFG.server.port}`) |
|||
}) |
|||
// Socket Io
|
|||
app.locals.io = require('socket.io')(server) |
|||
booth.setSocketIo(app.locals.io) |
|||
|
|||
return app |
|||
} |
|||
5
server/public/css/bootstrap.min.css
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,163 @@ |
|||
/* |
|||
* Globals |
|||
*/ |
|||
|
|||
/* Links */ |
|||
a, |
|||
a:focus, |
|||
a:hover { |
|||
color: #fff; |
|||
} |
|||
|
|||
/* Custom default button */ |
|||
.btn-default, |
|||
.btn-default:hover, |
|||
.btn-default:focus { |
|||
color: #333; |
|||
text-shadow: none; /* Prevent inheritence from `body` */ |
|||
background-color: #fff; |
|||
border: 1px solid #fff; |
|||
} |
|||
|
|||
|
|||
/* |
|||
* Base structure |
|||
*/ |
|||
|
|||
html, |
|||
body { |
|||
height: 100%; |
|||
background-color: #333; |
|||
} |
|||
body { |
|||
color: #fff; |
|||
text-align: center; |
|||
text-shadow: 0 1px 3px rgba(0,0,0,.5); |
|||
} |
|||
|
|||
/* Extra markup and styles for table-esque vertical and horizontal centering */ |
|||
.site-wrapper { |
|||
display: table; |
|||
width: 100%; |
|||
height: 100%; /* For at least Firefox */ |
|||
min-height: 100%; |
|||
-webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); |
|||
box-shadow: inset 0 0 100px rgba(0,0,0,.5); |
|||
} |
|||
.site-wrapper-inner { |
|||
display: table-cell; |
|||
vertical-align: top; |
|||
} |
|||
.cover-container { |
|||
margin-right: auto; |
|||
margin-left: auto; |
|||
} |
|||
|
|||
/* Padding for spacing */ |
|||
.inner { |
|||
padding: 30px; |
|||
} |
|||
|
|||
|
|||
/* |
|||
* Header |
|||
*/ |
|||
.masthead-brand { |
|||
margin-top: 10px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.masthead-nav > li { |
|||
display: inline-block; |
|||
} |
|||
.masthead-nav > li + li { |
|||
margin-left: 20px; |
|||
} |
|||
.masthead-nav > li > a { |
|||
padding-right: 0; |
|||
padding-left: 0; |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
color: #fff; /* IE8 proofing */ |
|||
color: rgba(255,255,255,.75); |
|||
border-bottom: 2px solid transparent; |
|||
} |
|||
.masthead-nav > li > a:hover, |
|||
.masthead-nav > li > a:focus { |
|||
background-color: transparent; |
|||
border-bottom-color: #a9a9a9; |
|||
border-bottom-color: rgba(255,255,255,.25); |
|||
} |
|||
.masthead-nav > .active > a, |
|||
.masthead-nav > .active > a:hover, |
|||
.masthead-nav > .active > a:focus { |
|||
color: #fff; |
|||
border-bottom-color: #fff; |
|||
} |
|||
|
|||
@media (min-width: 768px) { |
|||
.masthead-brand { |
|||
float: left; |
|||
} |
|||
.masthead-nav { |
|||
float: right; |
|||
} |
|||
} |
|||
|
|||
|
|||
/* |
|||
* Cover |
|||
*/ |
|||
|
|||
.cover { |
|||
padding: 0 20px; |
|||
} |
|||
.cover .btn-lg { |
|||
padding: 10px 20px; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
|
|||
/* |
|||
* Footer |
|||
*/ |
|||
|
|||
.mastfoot { |
|||
color: #999; /* IE8 proofing */ |
|||
color: rgba(255,255,255,.5); |
|||
} |
|||
|
|||
|
|||
/* |
|||
* Affix and center |
|||
*/ |
|||
|
|||
@media (min-width: 768px) { |
|||
/* Pull out the header and footer */ |
|||
.masthead { |
|||
position: fixed; |
|||
top: 0; |
|||
} |
|||
.mastfoot { |
|||
position: fixed; |
|||
bottom: 0; |
|||
} |
|||
/* Start the vertical centering */ |
|||
.site-wrapper-inner { |
|||
vertical-align: middle; |
|||
} |
|||
/* Handle the widths */ |
|||
.masthead, |
|||
.mastfoot, |
|||
.cover-container { |
|||
width: 100%; /* Must be percentage or pixels for horizontal alignment */ |
|||
} |
|||
} |
|||
|
|||
@media (min-width: 992px) { |
|||
.masthead, |
|||
.mastfoot, |
|||
.cover-container { |
|||
width: 700px; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
body { |
|||
padding: 50px; |
|||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; |
|||
} |
|||
|
|||
a { |
|||
color: #00B7FF; |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
html, |
|||
body { |
|||
height: 100%; |
|||
background-color: #333; |
|||
} |
|||
body { |
|||
color: #fff; |
|||
text-align: center; |
|||
text-shadow: 0 1px 3px rgba(0,0,0,.5); |
|||
} |
|||
7
server/public/js/bootstrap.min.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
4
server/public/js/jquery-1.11.2.min.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
3
server/public/js/socket.io-1.3.4.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
8
server/public/js/socket.io.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1
server/public/js/socket.io.js.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,172 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<title>Bobinoscope</title> |
|||
<!-- Latest compiled and minified CSS --> |
|||
<link rel="stylesheet" href="/css/view.css"/> |
|||
</head> |
|||
<body> |
|||
<div id="idMain" style="width:400px; margin: auto"> |
|||
<p>Cliquez sur le <i>bobinogramme</i> qui vous intéresse<br/>pour afficher le menu</p> |
|||
<div id="idViewLoading"> |
|||
<img src='/waiting.gif'/> |
|||
</div> |
|||
<div id="idViewContainer"> |
|||
|
|||
</div> |
|||
</div> |
|||
<div id="idMenu" style="display:none"> |
|||
<div id="idMenu-title">Bobinoscope</div> |
|||
<img id="idMenu-preview" src=""/> |
|||
<br/> |
|||
<div> |
|||
<button id="idMenu-btnDownload">Télécharger</button> |
|||
<button id="idMenu-btnPrint">Imprimer</button> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
<!-- Latest compiled and minified JQuery --> |
|||
<script src="/js/jquery-1.11.2.min.js"></script> |
|||
<!-- Getting the Socket.IO Client --> |
|||
<script src="/js/socket.io.js"></script> |
|||
</html> |
|||
|
|||
<style type="text/css"> |
|||
#idMenu { |
|||
background: #000; |
|||
opacity: 1; |
|||
position: fixed; |
|||
width: 100%; |
|||
padding: 1em; |
|||
top:0; |
|||
left:0; |
|||
bottom: 0; |
|||
right: 0; |
|||
font-size: 1.2em; |
|||
} |
|||
|
|||
.classBoothItem { |
|||
margin: 5px 0 |
|||
} |
|||
|
|||
#idMenu button{ |
|||
padding: 0.5em; |
|||
} |
|||
|
|||
#idMenu-title { |
|||
padding: 0.3em; |
|||
font-size: 1.3em; |
|||
} |
|||
</style> |
|||
|
|||
<script type="text/javascript"> |
|||
|
|||
var socket = io('/'); |
|||
socket.on('connect', function() { |
|||
console.log("connect") |
|||
}) |
|||
socket.on('boothState', function (data) { |
|||
BOB_getBooths(); |
|||
}) |
|||
|
|||
$(document).ready(function() { |
|||
BOB_getBooths(); |
|||
|
|||
if( $(window).width() < 500 ) { |
|||
$('#idMain').css('width' ,'100%'); |
|||
$('#idMenu-preview').css('width', 60 * $(window).width() / 100 ) |
|||
} else { |
|||
$('#idMenu-preview').css('width', 500 ) |
|||
} |
|||
|
|||
$('#idMenu').click(function() { |
|||
$(this).fadeOut(); |
|||
}) |
|||
|
|||
$('#idMenu-btnDownload').click(function() { |
|||
if( confirm("Télécharger le bobinogramme ?\n\nPour les iPhone-istes, un nouvel onglet va s'ouvrir avec le bobinogramme voulu. Un click long sur celui-ci permet de la télécharger définitivement.") ) { |
|||
window.open("/booth/build/result/"+BOB_var_selectedBoothId, "_blank"); |
|||
} |
|||
}) |
|||
|
|||
$('#idMenu-btnPrint').click(function() { |
|||
console.log(BOB_var_printer.todos, BOB_var_selectedBoothId) |
|||
if( BOB_var_printer.todos.indexOf(BOB_var_selectedBoothId) === -1 ) { |
|||
if( confirm("Envoyer une demande d'impression du bobinogramme pour le livre d'Or?\n\nPour que tout le monde puisse y mettre sa bobine, ne faites qu'une seule demande par groupe.") ) { |
|||
$.get("/booth/print/"+BOB_var_selectedBoothId, function(pResponse) { |
|||
if( pResponse.error ) { |
|||
console.log("Printer: ", pResponse.error) |
|||
alert("Echec ! Essayez d'envoyer une nouvelle demande") |
|||
} else { |
|||
alert("Demande prise en compte !\n\nVotre bobinogramme sera disponible près du Livre d'Or d'ici 1 heure") |
|||
} |
|||
}) |
|||
} |
|||
} else { |
|||
alert("Une demande a déjà été envoyée.\nVérifiez près du Livre d'Or si votre bobinogramme ne s'y trouve pas.") |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
var BOB_var_lastBooth = "0"; |
|||
var BOB_var_selectedBoothId = ""; |
|||
var BOB_var_printer = { |
|||
todos : [] |
|||
} |
|||
|
|||
function BOB_getBooths() { |
|||
var boothList = $(".classBoothItem"); |
|||
var jqBoothContainer = $('#idViewContainer'); |
|||
|
|||
$.get('/booth/toprint', function(pResponse) { |
|||
if( pResponse.error ) { |
|||
console.log('Printer: ', pResponse.error) |
|||
} else { |
|||
BOB_var_printer.todos = pResponse.todos |
|||
} |
|||
}) |
|||
|
|||
$.get('/booth/list/'+BOB_var_lastBooth, function (pResponse) { |
|||
if( pResponse.inProgress > 0 ) { |
|||
$('#idViewLoading').fadeIn(); |
|||
} else { |
|||
$('#idViewLoading').fadeOut(); |
|||
} |
|||
|
|||
pResponse.booths.forEach(function (pBoothName) { |
|||
var jqImg = $('<img>'); |
|||
var jqMenu = $('<div>'); |
|||
var jqBtnDownload = $('<button>'); |
|||
var jqBtnPrint = $('<button>'); |
|||
jqImg.css('width', '100%'); |
|||
jqImg.css('opacity', '0'); |
|||
jqImg.css('transition', 'opacity 1s'); |
|||
jqImg.data('booth', pBoothName); |
|||
jqImg.attr('src', "/booth/build/result/low/" + pBoothName); |
|||
jqImg.addClass('classBoothItem'); |
|||
|
|||
jqImg.on('click', function() { |
|||
BOB_var_selectedBoothId = pBoothName; |
|||
$('#idMenu-preview').attr('src', "/booth/build/result/low/" + pBoothName); |
|||
$('#idMenu').fadeIn(); |
|||
}) |
|||
|
|||
jqImg.on('load', function() { |
|||
jqImg.css('opacity', '1'); |
|||
}) |
|||
|
|||
if( pBoothName.localeCompare(BOB_var_lastBooth) > 0 ) { |
|||
BOB_var_lastBooth = pBoothName; |
|||
} |
|||
|
|||
jqBoothContainer.prepend(jqImg); |
|||
}) |
|||
}) |
|||
|
|||
console.log('-----------------'); |
|||
} |
|||
|
|||
</script> |
|||
@ -0,0 +1,226 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<title>Studio</title> |
|||
<!-- Latest compiled and minified CSS --> |
|||
<link rel="stylesheet" href="/css/bootstrap.min.css"> |
|||
<link rel="stylesheet" href="/css/cover.css"/> |
|||
</head> |
|||
<body> |
|||
<div class="site-wrapper"> |
|||
<div class="site-wrapper-inner"> |
|||
<div class="cover-container"> |
|||
<div class="inner cover" style="height:900px;position:relative"> |
|||
|
|||
<div id="idBoothPanel" style="position:absolute;width:100%"> |
|||
<div style="padding-top:1em"> |
|||
<div id="idViewCountdown" style="font-size:20em; display:none"></div> |
|||
<div id="idViewTrigger" style="display:block"> |
|||
<h1 class="cover-heading">Prends ta bobine !</h1> |
|||
<br/><br/> |
|||
<div style="width:800px;height:600px;position:relative;display:none"> |
|||
<video id="player" autoplay="true" style="height:100%;width:100%;position:absolute;left:0;top:0"></video> |
|||
<div style="position:absolute;background:#3C0000;top:0;left:0;width:<%= killZone.left%>%;height:100%;opacity:0.9"></div> |
|||
<div style="position:absolute;background:#3C0000;top:0;right:0;width:<%= killZone.right%>%;height:100%;opacity:0.9"></div> |
|||
<div style="position:absolute;background:#3C0000;top:0;left:<%= killZone.left%>%;right:<%= killZone.right%>%;height:<%= killZone.top%>%;opacity:0.9"></div> |
|||
<div style="position:absolute;background:#3C0000;bottom:0;left:<%= killZone.left%>%;right:<%= killZone.right%>%;height:<%= killZone.bottom%>%;opacity:0.9"></div> |
|||
</div> |
|||
</div> |
|||
<div id="idViewGenerating" style="display:none"> |
|||
<h1 class="cover-heading">Bobinogramme <i>en cours de génération...</i></h1> |
|||
<br/><br/> |
|||
<h3>Merci de bien vouloir patienter<br/>une quinzaine de secondes</h3> |
|||
<br/> |
|||
<br/> |
|||
<div style="position:relative;width:100%;height:2px;background:#444"> |
|||
<div id="idProgressBar" style="background:#fff;height:2px; width:0%;transition: all 200ms ease"></div> |
|||
</div> |
|||
</div> |
|||
<div id="idViewBobinogramme" style="display:none"> |
|||
<img src="" height="600px"/> |
|||
<!-- <h3 class="cover-heading">Retrouve ton <b>bobinogramme</b> sur <br/>http://<%= serverIpAddress %>:<%= serverPort %></h3> --> |
|||
</div> |
|||
</div> |
|||
<div id="idViewDoIt" style="font-size:8em; height:600px; display:none"> |
|||
Faites<br/> |
|||
<span id="idViewDoIt-theme" style="font-size:2em">le chat</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="idPictPreview" style="position:absolute;display:none;width:100%"> |
|||
<img src="" height="900px"> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="mastfoot"> |
|||
<div class="inner"> |
|||
<p>BW Bros.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</body> |
|||
<!-- Latest compiled and minified JQuery --> |
|||
<script src="/js/jquery-1.11.2.min.js"></script> |
|||
<!-- Latest compiled and minified javaScript bootstrap--> |
|||
<script src="/js/bootstrap.min.js"></script> |
|||
<!-- Getting the Socket.IO Client --> |
|||
<script src="/js/socket.io.js"></script> |
|||
</html> |
|||
|
|||
<style type="text/css"> |
|||
* { |
|||
-webkit-box-sizing: border-box; |
|||
box-sizing: border-box; |
|||
} |
|||
</style> |
|||
|
|||
<script type="text/javascript"> |
|||
|
|||
var socket = io('/'); |
|||
|
|||
socket.on('connect', function() { |
|||
console.log("connect") |
|||
}) |
|||
|
|||
socket.on('boothClick', function(pData) { |
|||
BOB_runBooth(); |
|||
}); |
|||
|
|||
var BOB_var_context = { |
|||
boothOnGoing : false, |
|||
initTimer : 3, |
|||
onGoingTimer : 0, |
|||
boothId : null, |
|||
pictureId : 0 |
|||
} |
|||
|
|||
var BOB_var_themes = { |
|||
selectedId : 0, |
|||
list : [ |
|||
"le chat", |
|||
"le cuir moustache", |
|||
"le roi", |
|||
"le bouffon", |
|||
"le moussailon", |
|||
"le geek", |
|||
"l'amoureux", |
|||
] |
|||
} |
|||
|
|||
$(document).ready(function() { |
|||
$(document).keypress(function (e) { |
|||
if (e.which === 32) BOB_runBooth() |
|||
}) |
|||
}) |
|||
|
|||
function BOB_runBooth() { |
|||
if( BOB_var_context.boothOnGoing === false ) { |
|||
var currentDate = new Date(); |
|||
BOB_var_context.boothOnGoing = true; |
|||
BOB_var_context.boothId = currentDate.getTime(); |
|||
BOB_var_context.pictureId = 0; |
|||
BOB_var_context.onGoingTimer = BOB_var_context.initTimer; |
|||
|
|||
$('#idViewTrigger').fadeOut(500, function() { |
|||
BOB_countdown(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
function BOB_countdown() { |
|||
$('#idViewCountdown').fadeIn(); |
|||
$('#idViewCountdown').html(BOB_var_context.onGoingTimer); |
|||
// |
|||
if( BOB_var_context.onGoingTimer > 5 ) { |
|||
// $('#idViewDoIt').show(); |
|||
// $('#idViewDoIt-theme').hide(); |
|||
} else if( BOB_var_context.onGoingTimer > 2 ) { |
|||
// $('#idViewDoIt-theme').html( BOB_var_themes.list[BOB_var_themes.selectedId] ); |
|||
// $('#idViewDoIt-theme').show(); |
|||
} |
|||
|
|||
if( BOB_var_context.onGoingTimer > 0 ) { |
|||
setTimeout(function() { |
|||
BOB_var_context.onGoingTimer -= 1; |
|||
BOB_countdown(); |
|||
}, 1000) |
|||
} else { |
|||
$('#idViewCountdown').html("Smile"); |
|||
BOB_takeThe1Pictures(); |
|||
} |
|||
} |
|||
|
|||
function BOB_displayPictPreview(pBoothId, pPictureId, pCallback) { |
|||
$('#idViewCountdown').html(""); |
|||
$('#idPictPreview > img').attr('src', '/img/prebuilt/'+pBoothId+'/pict_'+pPictureId+'.jpg'); |
|||
$('#idBoothPanel').fadeOut(50, function() { |
|||
$('#idPictPreview').show(); |
|||
setTimeout(function() { |
|||
$('#idPictPreview').hide(); |
|||
$('#idBoothPanel').fadeIn(50, function() { |
|||
pCallback() |
|||
}) |
|||
}, 4000) |
|||
}) |
|||
} |
|||
|
|||
var BOB_var_progressTimer = new Date(); |
|||
|
|||
function BOB_lauchFakeProgressBar() { |
|||
setTimeout(function() { |
|||
var progress = (new Date() - BOB_var_progressTimer) / <%= renderingTime %> * 100; |
|||
$('#idProgressBar').css('width', progress+'%'); |
|||
if( progress < 100 ) { |
|||
BOB_lauchFakeProgressBar(); |
|||
} |
|||
}, 500) |
|||
} |
|||
|
|||
|
|||
function BOB_takeThe1Pictures() { |
|||
$.get('/booth/dslr/takepicture/'+BOB_var_context.boothId+'/'+BOB_var_context.pictureId, function(pResponse) { |
|||
if( pResponse.error ) { |
|||
if( confirm(pResponse.error) ) { |
|||
BOB_var_context.boothOnGoing = false; |
|||
$('#idViewCountdown').fadeOut(500, function() { |
|||
$('#idViewTrigger').fadeIn(); |
|||
}); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
BOB_var_context.pictureId += 1; |
|||
if( BOB_var_context.pictureId < 4 ) { |
|||
BOB_var_context.onGoingTimer = BOB_var_context.initTimer; |
|||
BOB_countdown(); |
|||
} else { |
|||
$('#idViewCountdown').fadeOut(500, function() { |
|||
BOB_var_progressTimer = new Date(); |
|||
$('#idProgressBar').css('width', '0'); |
|||
$('#idViewGenerating').fadeIn(); |
|||
BOB_lauchFakeProgressBar() |
|||
}) |
|||
$.get('/booth/build/'+BOB_var_context.boothId, function(pResponse) { |
|||
$('#idViewBobinogramme > img').attr('src', '/booth/build/result/low/'+pResponse.boothId+'.jpg'); |
|||
$('#idViewGenerating').fadeOut(500, function() { |
|||
$('#idViewBobinogramme').fadeIn(500, function() { |
|||
setTimeout(function(){ |
|||
BOB_var_context.boothOnGoing = false; |
|||
$('#idViewBobinogramme').fadeOut(500, function() { |
|||
$('#idViewTrigger').fadeIn(); |
|||
}); |
|||
}, 10000) |
|||
}) |
|||
}) |
|||
}); |
|||
} |
|||
}) |
|||
} |
|||
|
|||
</script> |
|||
@ -0,0 +1,62 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<title>Bobinoscope</title> |
|||
<!-- Latest compiled and minified CSS --> |
|||
<link rel="stylesheet" href="/css/view.css"/> |
|||
</head> |
|||
<body> |
|||
<div id="idMain" style="margin: auto"> |
|||
<p> |
|||
<button onclick="takePicture()">Prendre une photo</button> |
|||
</p> |
|||
<div id="idViewLoading"> |
|||
<img src='/waiting2.gif'/> |
|||
</div> |
|||
<div id="idViewContainer"> |
|||
|
|||
</div> |
|||
</div> |
|||
</body> |
|||
<!-- Latest compiled and minified JQuery --> |
|||
<script src="/js/jquery-1.11.2.min.js"></script> |
|||
</html> |
|||
|
|||
<style type="text/css"> |
|||
#idViewContainer { |
|||
background-size: contain; |
|||
background-repeat: no-repeat; |
|||
background-position: center; |
|||
/* background-origin: */ |
|||
} |
|||
</style> |
|||
|
|||
<script type="text/javascript"> |
|||
|
|||
// var socket = io('http://<%= serverIpAddress %>:<%= serverPort %>'); |
|||
// socket.on('connect', function() { |
|||
// console.log("connect") |
|||
// }) |
|||
// socket.on('boothState', function (data) { |
|||
// BOB_getBooths(); |
|||
// }) |
|||
|
|||
$(document).ready(function() { |
|||
$('#idViewLoading').hide() |
|||
$('#idViewContainer').css('height', 90 * $(document).height() / 100) |
|||
// console.log() |
|||
}) |
|||
|
|||
function takePicture () { |
|||
$('#idViewLoading').fadeIn() |
|||
|
|||
$.get('/booth/test/takepicture', function (pResponse) { |
|||
$('#idViewLoading').fadeOut() |
|||
$('#idViewContainer').css('backgroundImage', 'url("/booth/test/takepicture/result?time='+(new Date()).getTime()+'")') |
|||
}) |
|||
} |
|||
|
|||
</script> |
|||
@ -0,0 +1,107 @@ |
|||
const exec = require('child_process').exec |
|||
const path = require('path') |
|||
const async = require('async') |
|||
const splitLines = require('split-lines') |
|||
const mkdirp = require('mkdirp') |
|||
const fs = require('fs-extra') |
|||
|
|||
require('colors') |
|||
|
|||
var CFG = require('../config') |
|||
|
|||
let tools = {} |
|||
|
|||
function execGphoto2 (pArgs, pCallback) { |
|||
exec(`gphoto2 ${pArgs}`, function (pErr, pStdout, pStderr) { |
|||
if (pErr) { |
|||
console.log(`[!] Please install gphoto2`.red) |
|||
console.log(`sudo apt-get install gphoto2`) |
|||
process.exit(1) |
|||
} |
|||
let error = null |
|||
let outInLines = [] |
|||
if (/Error/i.test(pStderr)) { |
|||
error = pStderr |
|||
} |
|||
if (pStdout) { |
|||
outInLines = splitLines(pStdout) |
|||
} |
|||
pCallback(error, outInLines) |
|||
}) |
|||
} |
|||
|
|||
tools.checkVersion = function (pCb) { |
|||
console.log('[-] Checking gphoto2 installation...'.bgBlue) |
|||
execGphoto2('--version', function (pErr, pStdout) { |
|||
console.log(`OK: ${pStdout[0]}`.green) |
|||
pCb() |
|||
}) |
|||
} |
|||
|
|||
tools.checkCameraConnection = function (pCb) { |
|||
console.log('[-] Checking camera connection...'.bgBlue) |
|||
execGphoto2('--auto-detect', function (pErr, pStdout) { |
|||
let device = pStdout[2] |
|||
if (!device) { |
|||
console.log(`[!] Camera not connected`.red) |
|||
console.log('1. Connect camera via USB') |
|||
console.log('2. Turn on the camera') |
|||
process.exit(1) |
|||
} |
|||
console.log(`OK: ${device}`.green) |
|||
pCb() |
|||
}) |
|||
} |
|||
|
|||
tools.__takeOnePicture = function (pPath, pCb) { |
|||
execGphoto2(`--capture-image-and-download --filename=${pPath} --force-overwrite`, pCb) |
|||
} |
|||
|
|||
tools.takeOnePicture = function (pBoothId, pPictId, pCallback) { |
|||
let destPict = '/tmp/bobine.jpg' |
|||
var destPath = path.join(CFG.paths.original, pBoothId) |
|||
var pictOrig = path.join(destPath, CFG.pictNames[pPictId]) |
|||
|
|||
mkdirp.sync(path.join(CFG.paths.prebuilt, pBoothId)) |
|||
|
|||
tools.__takeOnePicture(destPict, function (pErr) { |
|||
// If error occurs
|
|||
if (pErr) return pCallback(pErr) |
|||
mkdirp(destPath, function () { |
|||
fs.copy(destPict, pictOrig, function (pErr) { |
|||
return pCallback(pErr, pictOrig) |
|||
}) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
tools.init = function (pCb) { |
|||
let tasks = [] |
|||
tasks.push(tools.checkVersion) |
|||
tasks.push(tools.checkCameraConnection) |
|||
tasks.push(function (pCb) { |
|||
let output = '/tmp/bobine-dummy.jpg' |
|||
console.log('[-] Taking dummy photo...'.bgBlue) |
|||
tools.__takeOnePicture(output, function (pErr) { |
|||
if (pErr) { |
|||
console.log(`[!] Failed to take picture`.red) |
|||
console.log(pErr.grey) |
|||
console.log('1. Add one SDCard into the device') |
|||
console.log('2. Take one picture manually') |
|||
process.exit(1) |
|||
} |
|||
console.log(`OK: ${output}`.green) |
|||
pCb() |
|||
}) |
|||
|
|||
// opn(output)
|
|||
// .then(function () {
|
|||
// console.log('OK'.green)
|
|||
// pCb()
|
|||
// })
|
|||
}) |
|||
|
|||
async.waterfall(tasks, pCb) |
|||
} |
|||
|
|||
module.exports = tools |
|||
@ -0,0 +1,155 @@ |
|||
var gm = require('gm').subClass({ imageMagick: true }) |
|||
var async = require('async') |
|||
var path = require('path') |
|||
var fs = require('fs') |
|||
var mkdirp = require('mkdirp') |
|||
|
|||
var CFG = require('../config') |
|||
|
|||
var tools = {} |
|||
|
|||
tools.getBoothList = function (pFrom, pCallback) { |
|||
fs.readdir(CFG.paths.final, function (pErr, pBoothList) { |
|||
var boothList = pBoothList ? pBoothList.sort() : [] |
|||
boothList = boothList.splice(boothList.indexOf(pFrom) + 1) |
|||
pCallback(pErr, boothList) |
|||
}) |
|||
} |
|||
|
|||
tools.buildBooth = function (pBoothId, pCallback) { |
|||
// Create output prebuild directory
|
|||
mkdirp.sync(path.join(CFG.paths.prebuilt, pBoothId)) |
|||
// Resize pictures
|
|||
async.mapSeries(CFG.pictNames, |
|||
function (pPictName, pCb) { |
|||
var srcPict = path.join(CFG.paths.original, pBoothId, pPictName) |
|||
var dstPict = path.join(CFG.paths.prebuilt, pBoothId, pPictName) |
|||
|
|||
console.log('Resizing ' + dstPict) |
|||
|
|||
gm(srcPict) |
|||
.autoOrient() |
|||
.gravity('Center') |
|||
.resize(CFG.cropSize.width, CFG.cropSize.height, '^') |
|||
.crop(CFG.cropSize.width, CFG.cropSize.height) |
|||
.write(dstPict, function (pErr) { |
|||
if (pErr) console.log('Failed to resize picture', pErr) |
|||
pCb() |
|||
}) |
|||
}, |
|||
function (pErr) { |
|||
console.log('Building booth') |
|||
switch (CFG.style) { |
|||
default : |
|||
BOB_generateBooth(pBoothId, 'default', pCallback) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
function BOB_generateBooth (pBoothId, pType, pCallback) { |
|||
var prebuiltPath = path.join(CFG.paths.prebuilt, pBoothId) |
|||
var finalPict = path.join(CFG.paths.final, pBoothId + '.jpg') |
|||
var finalLdPict = path.join(CFG.paths.final_ld, pBoothId + '.jpg') |
|||
var outPict = gm(CFG.booths[pType].template) |
|||
|
|||
var printedDate = new Date(parseInt(pBoothId)) |
|||
|
|||
outPict.fill(CFG.background) |
|||
outPict.drawRectangle(0, 0, CFG.booths[pType].resolution.width, CFG.booths[pType].resolution.height) |
|||
// Draw footer
|
|||
outPict.draw('image Over 0,2100 1600,250 \'' + CFG.footer + '\'') |
|||
|
|||
CFG.pictNames.forEach(function (pPictName, pIndex) { |
|||
var cmdDraw = 'image Over' |
|||
cmdDraw += ' ' + CFG.booths[pType].layout[pIndex].x |
|||
cmdDraw += ',' + CFG.booths[pType].layout[pIndex].y |
|||
cmdDraw += ' ' + CFG.booths[pType].layout[pIndex].width |
|||
cmdDraw += ',' + CFG.booths[pType].layout[pIndex].height |
|||
cmdDraw += ' \'' + path.join(prebuiltPath, pPictName) + '\'' |
|||
|
|||
// console.log(cmdDraw);
|
|||
outPict.draw(cmdDraw) |
|||
}) |
|||
|
|||
outPict.fill('#666') |
|||
outPict.pointSize(20) |
|||
outPict.draw('text 1250,2350 "' + printedDate.toUTCString() + '"') |
|||
|
|||
outPict.write(finalPict, function (pErr) { |
|||
if (pErr) { |
|||
console.log('Write outfile') |
|||
console.log(pErr) |
|||
pCallback(pErr) |
|||
} else { |
|||
gm(finalPict) |
|||
.resize(400, null) |
|||
.write(finalLdPict, function (pErr) { |
|||
pCallback(pErr) |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
module.exports = tools |
|||
|
|||
tools.init = function () { |
|||
BOB_updateConfig() |
|||
} |
|||
|
|||
// function (pConfig) {
|
|||
// CFG = pConfig
|
|||
// return tools
|
|||
// }
|
|||
|
|||
function BOB_updateConfig () { |
|||
fs.readdir(CFG.paths.template, function (pErr, pFiles) { |
|||
if (pErr) { |
|||
console.log('BOB_updateConfig', 'Path:', CFG.paths.template, pErr) |
|||
} else { |
|||
async.map(pFiles, |
|||
function (pFile, pCb) { |
|||
// Get name of template
|
|||
var boothName = pFile.split('.')[0] |
|||
// Init layouts object for the template
|
|||
CFG.booths[boothName] = { |
|||
name: boothName, |
|||
template: path.join(CFG.paths.template, pFile), |
|||
resolution: { width: 0, height: 0 }, |
|||
layout: [] |
|||
} |
|||
// Get reslution of current template
|
|||
gm(CFG.booths[boothName].template).size(function (pErr, pValue) { |
|||
if (pErr) { |
|||
console.log('[ERROR] Get template size', pErr) |
|||
} else { |
|||
CFG.booths[boothName].resolution = pValue |
|||
BOB_generatePictLayout(boothName) |
|||
} |
|||
pCb() |
|||
}) |
|||
}, |
|||
function (pErr) { |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
function BOB_generatePictLayout (pBoothName) { |
|||
var index = 0 |
|||
var marginX = 20 |
|||
var marginY = 20 |
|||
|
|||
var pictWidth = (CFG.booths[pBoothName].resolution.width - 3 * marginX) / 2 |
|||
var pictHeight = parseInt(pictWidth * CFG.cropSize.height / CFG.cropSize.width) |
|||
|
|||
CFG.pictNames.forEach(function (pPictName, pIndex) { |
|||
CFG.booths[pBoothName].layout.push({ |
|||
x: marginX + (marginX + pictWidth) * (pIndex % 2), |
|||
y: marginY + (marginY + pictHeight) * parseInt(pIndex / 2), |
|||
width: pictWidth, |
|||
height: pictHeight |
|||
}) |
|||
}) |
|||
|
|||
console.log('INIT DONE') |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save