Initial commit

pull/9/head
rprieto 10 years ago
commit 62c0d09868

3
.gitignore vendored

@ -0,0 +1,3 @@
node_modules
example/website
.DS_Store

@ -0,0 +1,80 @@
# thumbsup
Static HTML galleries from a list of photos & videos.
- creates thumbnails for fast previews
- uses relative paths so you can deploy the pages anywhere
- supports custom CSS for styling
- works great with Amazon S3 for static hosting
![screenshot](https://raw.github.com/rprieto/thumbsup/master/screenshot.jpg)
*Note: `thumbsup` keeps generated content separate from the original media. This means you're free to upload the media anywhere, and never have to worry about deleting the output folder*
## Requirements
- [Node.js](http://nodejs.org/): `brew install Node`
- [GraphicsMagick](http://www.graphicsmagick.org/): `brew install graphicsmagick`
## Input
Any folder with photos and videos.
`thumbsup` supports 1 level of subfolders:
```
input
|
|__ paris
| |__ img001.jpg
| |__ img002.jpg
|
|__ sydney
|__ vid001.mp4
|__ img003.png
```
## Generating the galleries
![npm install thumbsup](https://nodei.co/npm/thumbsup.png)
```
thumbsup <args>
```
The following args are required:
- `--input <path>` path to the folder with photos / videos
- `--output <path>` target output folder
- `--media-prefix <url>` prefix for the photos / videos URLS (can be relative or absolute)
And you can optionally specify:
- `--size <pixels>` size of the thumbnails
- `--css <path>` use the given CSS file instead of the default style
For example:
```bash
thumbsup --input "/media/photos" --output "./website" --media-prefix "http://my.photo.bucket.s3.amazon.com" --css "custom.css" --size 200
```
## Deployment
The simplest is to deploy the media and generated pages to S3 buckets on AWS using the [AWS CLI tools](http://aws.amazon.com/cli/).
- `aws s3 sync /media/photos s3://my.photo.bucket --delete`
- `aws s3 sync /generated/website s3://my.website.bucket --delete`
## Password protection
Amazon S3 buckets do not offer any type of authentication. However you can choose to deploy to another web server that offers password protection, such as HTTP Basic Auth.
An alternative is to deploy the galleries to UUID-based locations, like Dropbox shared galleries.
## Dev notes
To create the sample gallery locally:
```
npm run example
```

@ -0,0 +1,19 @@
var thumbsup = require('../src/index');
thumbsup.build({
// the input folder
// with all photos/videos
input: 'example/media',
// the output folder
// for the thumbnails and static pages
output: 'example/website',
// relative path to the media
// a local path for testing
// but this could be the URL to an S3 bucket
mediaPrefix: '../media'
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

@ -0,0 +1,27 @@
{
"name": "static-gallery",
"version": "0.0.1",
"description": "Photo / video gallery generator",
"author": "Romain Prieto",
"license": "BSD",
"keywords": [
"photo", "video", "gallery", "thumbnails", "portfolio",
"website", "s3", "generator"
],
"scripts": {
"example": "node example/build.js"
},
"bin": {
"static-gallery": "./bin/generate.js"
},
"dependencies": {
"gulp": "~3.6.0",
"handlebars": "~2.0.0-alpha.2",
"gulp-newer": "~0.3.0",
"concurrent-transform": "~1.0.0",
"gulp-image-resize": "~0.5.0",
"lodash": "~2.4.1",
"wrench": "~1.5.8",
"fs-extra": "~0.8.1"
}
}

@ -0,0 +1,71 @@
@charset "UTF-8";
/*
* blueimp Gallery Indicator CSS 1.1.0
* https://github.com/blueimp/Gallery
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
.blueimp-gallery > .indicator {
position: absolute;
top: auto;
right: 15px;
bottom: 15px;
left: 15px;
margin: 0 40px;
padding: 0;
list-style: none;
text-align: center;
line-height: 10px;
display: none;
}
.blueimp-gallery > .indicator > li {
display: inline-block;
width: 9px;
height: 9px;
margin: 6px 3px 0 3px;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
border: 1px solid transparent;
background: #ccc;
background: rgba(255, 255, 255, 0.25) center no-repeat;
border-radius: 5px;
box-shadow: 0 0 2px #000;
opacity: 0.5;
cursor: pointer;
}
.blueimp-gallery > .indicator > li:hover,
.blueimp-gallery > .indicator > .active {
background-color: #fff;
border-color: #fff;
opacity: 1;
}
.blueimp-gallery-controls > .indicator {
display: block;
/* Fix z-index issues (controls behind slide element) on Android: */
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
.blueimp-gallery-single > .indicator {
display: none;
}
.blueimp-gallery > .indicator {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* IE7 fixes */
*+html .blueimp-gallery > .indicator > li {
display: inline;
}

@ -0,0 +1,87 @@
@charset "UTF-8";
/*
* blueimp Gallery Video Factory CSS 1.3.0
* https://github.com/blueimp/Gallery
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
.blueimp-gallery > .slides > .slide > .video-content > img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
/* Prevent artifacts in Mozilla Firefox: */
-moz-backface-visibility: hidden;
}
.blueimp-gallery > .slides > .slide > .video-content > video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.blueimp-gallery > .slides > .slide > .video-content > iframe {
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: 100%;
border: none;
}
.blueimp-gallery > .slides > .slide > .video-playing > iframe {
top: 0;
}
.blueimp-gallery > .slides > .slide > .video-content > a {
position: absolute;
top: 50%;
right: 0;
left: 0;
margin: -64px auto 0;
width: 128px;
height: 128px;
background: url(../img/video-play.png) center no-repeat;
opacity: 0.8;
cursor: pointer;
}
.blueimp-gallery > .slides > .slide > .video-content > a:hover {
opacity: 1;
}
.blueimp-gallery > .slides > .slide > .video-playing > a,
.blueimp-gallery > .slides > .slide > .video-playing > img {
display: none;
}
.blueimp-gallery > .slides > .slide > .video-content > video {
display: none;
}
.blueimp-gallery > .slides > .slide > .video-playing > video {
display: block;
}
.blueimp-gallery > .slides > .slide > .video-loading > a {
background: url(../img/loading.gif) center no-repeat;
background-size: 64px 64px;
}
/* Replace PNGs with SVGs for capable browsers (excluding IE<9) */
body:last-child .blueimp-gallery > .slides > .slide > .video-content:not(.video-loading) > a {
background-image: url(../img/video-play.svg);
}
/* IE7 fixes */
*+html .blueimp-gallery > .slides > .slide > .video-content {
height: 100%;
}
*+html .blueimp-gallery > .slides > .slide > .video-content > a {
left: 50%;
margin-left: -64px;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="64" height="64">
<circle cx="32" cy="32" r="25" stroke="red" stroke-width="7" fill="black" fill-opacity="0.2"/>
<rect x="28" y="7" width="8" height="50" fill="red" transform="rotate(45, 32, 32)"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="30" height="15">
<polygon points="2,1 2,14 13,7" stroke="black" stroke-width="1" fill="white"/>
<rect x="17" y="2" width="4" height="11" stroke="black" stroke-width="1" fill="white"/>
<rect x="24" y="2" width="4" height="11" stroke="black" stroke-width="1" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="64" height="64">
<circle cx="32" cy="32" r="25" stroke="white" stroke-width="7" fill="black" fill-opacity="0.2"/>
<polygon points="26,22 26,42 43,32" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

@ -0,0 +1,153 @@
/*
* blueimp Gallery Indicator JS 1.1.0
* https://github.com/blueimp/Gallery
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define([
'./blueimp-helper',
'./blueimp-gallery'
], factory);
} else {
// Browser globals:
factory(
window.blueimp.helper || window.jQuery,
window.blueimp.Gallery
);
}
}(function ($, Gallery) {
'use strict';
$.extend(Gallery.prototype.options, {
// The tag name, Id, element or querySelector of the indicator container:
indicatorContainer: 'ol',
// The class for the active indicator:
activeIndicatorClass: 'active',
// The list object property (or data attribute) with the thumbnail URL,
// used as alternative to a thumbnail child element:
thumbnailProperty: 'thumbnail',
// Defines if the gallery indicators should display a thumbnail:
thumbnailIndicators: true
});
var initSlides = Gallery.prototype.initSlides,
addSlide = Gallery.prototype.addSlide,
resetSlides = Gallery.prototype.resetSlides,
handleClick = Gallery.prototype.handleClick,
handleSlide = Gallery.prototype.handleSlide,
handleClose = Gallery.prototype.handleClose;
$.extend(Gallery.prototype, {
createIndicator: function (obj) {
var indicator = this.indicatorPrototype.cloneNode(false),
title = this.getItemProperty(obj, this.options.titleProperty),
thumbnailProperty = this.options.thumbnailProperty,
thumbnailUrl,
thumbnail;
if (this.options.thumbnailIndicators) {
thumbnail = obj.getElementsByTagName && $(obj).find('img')[0];
if (thumbnail) {
thumbnailUrl = thumbnail.src;
} else if (thumbnailProperty) {
thumbnailUrl = this.getItemProperty(obj, thumbnailProperty);
}
if (thumbnailUrl) {
indicator.style.backgroundImage = 'url("' + thumbnailUrl + '")';
}
}
if (title) {
indicator.title = title;
}
return indicator;
},
addIndicator: function (index) {
if (this.indicatorContainer.length) {
var indicator = this.createIndicator(this.list[index]);
indicator.setAttribute('data-index', index);
this.indicatorContainer[0].appendChild(indicator);
this.indicators.push(indicator);
}
},
setActiveIndicator: function (index) {
if (this.indicators) {
if (this.activeIndicator) {
this.activeIndicator
.removeClass(this.options.activeIndicatorClass);
}
this.activeIndicator = $(this.indicators[index]);
this.activeIndicator
.addClass(this.options.activeIndicatorClass);
}
},
initSlides: function (reload) {
if (!reload) {
this.indicatorContainer = this.container.find(
this.options.indicatorContainer
);
if (this.indicatorContainer.length) {
this.indicatorPrototype = document.createElement('li');
this.indicators = this.indicatorContainer[0].children;
}
}
initSlides.call(this, reload);
},
addSlide: function (index) {
addSlide.call(this, index);
this.addIndicator(index);
},
resetSlides: function () {
resetSlides.call(this);
this.indicatorContainer.empty();
this.indicators = [];
},
handleClick: function (event) {
var target = event.target || event.srcElement,
parent = target.parentNode;
if (parent === this.indicatorContainer[0]) {
// Click on indicator element
this.preventDefault(event);
this.slide(this.getNodeIndex(target));
} else if (parent.parentNode === this.indicatorContainer[0]) {
// Click on indicator child element
this.preventDefault(event);
this.slide(this.getNodeIndex(parent));
} else {
return handleClick.call(this, event);
}
},
handleSlide: function (index) {
handleSlide.call(this, index);
this.setActiveIndicator(index);
},
handleClose: function () {
if (this.activeIndicator) {
this.activeIndicator
.removeClass(this.options.activeIndicatorClass);
}
handleClose.call(this);
}
});
return Gallery;
}));

@ -0,0 +1,171 @@
/*
* blueimp Gallery Video Factory JS 1.1.1
* https://github.com/blueimp/Gallery
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/* global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define([
'./blueimp-helper',
'./blueimp-gallery'
], factory);
} else {
// Browser globals:
factory(
window.blueimp.helper || window.jQuery,
window.blueimp.Gallery
);
}
}(function ($, Gallery) {
'use strict';
$.extend(Gallery.prototype.options, {
// The class for video content elements:
videoContentClass: 'video-content',
// The class for video when it is loading:
videoLoadingClass: 'video-loading',
// The class for video when it is playing:
videoPlayingClass: 'video-playing',
// The list object property (or data attribute) for the video poster URL:
videoPosterProperty: 'poster',
// The list object property (or data attribute) for the video sources array:
videoSourcesProperty: 'sources'
});
var handleSlide = Gallery.prototype.handleSlide;
$.extend(Gallery.prototype, {
handleSlide: function (index) {
handleSlide.call(this, index);
if (this.playingVideo) {
this.playingVideo.pause();
}
},
videoFactory: function (obj, callback, videoInterface) {
var that = this,
options = this.options,
videoContainerNode = this.elementPrototype.cloneNode(false),
videoContainer = $(videoContainerNode),
errorArgs = [{
type: 'error',
target: videoContainerNode
}],
video = videoInterface || document.createElement('video'),
url = this.getItemProperty(obj, options.urlProperty),
type = this.getItemProperty(obj, options.typeProperty),
title = this.getItemProperty(obj, options.titleProperty),
posterUrl = this.getItemProperty(obj, options.videoPosterProperty),
posterImage,
sources = this.getItemProperty(
obj,
options.videoSourcesProperty
),
source,
playMediaControl,
isLoading,
hasControls;
videoContainer.addClass(options.videoContentClass);
if (title) {
videoContainerNode.title = title;
}
if (video.canPlayType) {
if (url && type && video.canPlayType(type)) {
video.src = url;
} else {
while (sources && sources.length) {
source = sources.shift();
url = this.getItemProperty(source, options.urlProperty);
type = this.getItemProperty(source, options.typeProperty);
if (url && type && video.canPlayType(type)) {
video.src = url;
break;
}
}
}
}
if (posterUrl) {
video.poster = posterUrl;
posterImage = this.imagePrototype.cloneNode(false);
$(posterImage).addClass(options.toggleClass);
posterImage.src = posterUrl;
posterImage.draggable = false;
videoContainerNode.appendChild(posterImage);
}
playMediaControl = document.createElement('a');
playMediaControl.setAttribute('target', '_blank');
if (!videoInterface) {
playMediaControl.setAttribute('download', title);
}
playMediaControl.href = url;
if (video.src) {
video.controls = true;
(videoInterface || $(video))
.on('error', function () {
that.setTimeout(callback, errorArgs);
})
.on('pause', function () {
isLoading = false;
videoContainer
.removeClass(that.options.videoLoadingClass)
.removeClass(that.options.videoPlayingClass);
if (hasControls) {
that.container.addClass(that.options.controlsClass);
}
delete that.playingVideo;
if (that.interval) {
that.play();
}
})
.on('playing', function () {
isLoading = false;
videoContainer
.removeClass(that.options.videoLoadingClass)
.addClass(that.options.videoPlayingClass);
if (that.container.hasClass(that.options.controlsClass)) {
hasControls = true;
that.container.removeClass(that.options.controlsClass);
} else {
hasControls = false;
}
})
.on('play', function () {
window.clearTimeout(that.timeout);
isLoading = true;
videoContainer.addClass(that.options.videoLoadingClass);
that.playingVideo = video;
});
$(playMediaControl).on('click', function (event) {
that.preventDefault(event);
if (isLoading) {
video.pause();
} else {
video.play();
}
});
videoContainerNode.appendChild(
(videoInterface && videoInterface.element) || video
);
}
videoContainerNode.appendChild(playMediaControl);
this.setTimeout(callback, [{
type: 'load',
target: videoContainerNode
}]);
return videoContainerNode;
}
});
return Gallery;
}));

File diff suppressed because one or more lines are too long

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

@ -0,0 +1,87 @@
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
}
html, body {
padding: 0;
margin: 0;
}
body {
border-top: 5px solid #33609c;
font-family: 'Open Sans', sans-serif;
font-size: 16px;
font-weight: lighter;
margin: 0 1em;
padding: 0 1em;
-webkit-font-smoothing: antialiased;
}
header {
font-size: 2em;
padding: 1em 0;
}
h1 {
color: #33609c;
display: inline-block;
font-weight: bold;
}
h2 {
color: #666;
display: inline-block;
}
nav {
color: #33609c;
margin-bottom: 2em;
}
nav li {
border-radius: 2px;
display: inline-block;
padding: 0.4em 1em;
}
nav a {
color: #33609c;
text-decoration: none;
}
nav li.active {
background-color: #33609c;
}
nav li.active a {
color: #fff;
font-weight: bold;
}
nav li:not(.active):hover {
background-color: #eee;
}
.gallery li {
display: inline-block;
margin-right: 0.5em;
margin-bottom: 0.5em;
position: relative;
}
.gallery img {
border-radius: 2px;
}
.gallery .play-overlay {
color: #fff;
font-size: 2em;
height: 100%;
left: 0;
line-height: 100%;
position: absolute;
text-align: center;
top: 0;
width: 100%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@ -0,0 +1,41 @@
var _ = require('lodash');
var path = require('path');
var wrench = require('wrench');
exports.fromDisk = function(mediaPath, mediaPrefix) {
function fileInfo(file) {
return {
// read file date
date: 2340930845,
path: file,
url: mediaPrefix + '/' + file,
thumbnail: 'thumbs/' + file,
video: file.match(/\.mp4$/)
}
}
function byFolder(file) {
return path.dirname(file.path);
}
function byExtension(file) {
return file.match(/\.(jpg|jpeg|png|mp4)$/)
}
function folderInfo(files, name) {
return {
name: name,
media: files,
url: name + '.html'
};
}
var files = wrench.readdirSyncRecursive(mediaPath);
return _(files).filter(byExtension)
.map(fileInfo)
.groupBy(byFolder)
.map(folderInfo)
.value();
};

@ -0,0 +1,53 @@
var _ = require('lodash');
var fs = require('fs-extra');
var os = require('os');
var path = require('path');
var gulp = require('gulp');
var newer = require('gulp-newer');
var imageResize = require('gulp-image-resize');
var parallel = require('concurrent-transform');
var galleries = require('./galleries');
var render = require('./render');
exports.build = function(opts) {
fs.mkdirp(opts.output);
var list = galleries.fromDisk(opts.input, opts.mediaPrefix);
console.log(require('util').inspect(list, {depth:6, color:true}))
gulp.task('thumbs', function () {
var dest = opts.output + '/thumbs';
gulp
.src(opts.input + '/**/*.{jpg,png}')
.pipe(newer(dest))
.pipe(parallel(imageResize({width: 100, height: 100, crop: true}), os.cpus().length))
.pipe(gulp.dest(dest));
});
gulp.task('index', function() {
// render('index.hbs', {salt: SALT}, 'index.html');
});
gulp.task('public', function() {
var dest = opts.output + '/public';
gulp
.src('public/**')
.pipe(newer(dest))
.pipe(gulp.dest(dest));
});
gulp.task('galleries', function() {
// var dest = opts.output + '/galleries';
// wrench.rmdirSyncRecursive(dest, true);
// fs.mkdirp(dest);
list.forEach(function(folder) {
var rendered = render.gallery(list, folder);
var outputPath = path.join(opts.output, folder.url);
fs.writeFileSync(outputPath, rendered);
});
});
gulp.run('thumbs', 'public', 'index', 'galleries');
};

@ -0,0 +1,32 @@
var fs = require('fs');
var path = require('path');
var handlebars = require('handlebars');
function compileTemplate(hbsFile) {
var src = fs.readFileSync(path.join('templates', hbsFile));
return handlebars.compile(src.toString());
}
var indexTemplate = compileTemplate('index.hbs');
var galleryTemplate = compileTemplate('gallery.hbs');
exports.index = function(list) {
};
exports.gallery = function(list, active) {
var links = list.map(function(item) {
return {
name: item.name,
url: item.name + '.html',
active: (item === active)
};
});
return galleryTemplate({
links: links,
gallery: active
});
};

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{gallery.name}}</title>
<link rel="stylesheet" href="public/reset.css" />
<link rel="stylesheet" href="public/theme.css" />
<link rel="stylesheet" href="public/blueimp/css/blueimp-gallery.min.css">
<link rel="stylesheet" href="public/blueimp/css/blueimp-gallery-video.css">
<link rel="stylesheet" href="public/blueimp/css/blueimp-gallery-indicator.css">
</head>
<body>
<header>
<h1>Photo</h1>
<h2>gallery</h2>
</header>
<nav>
<ul>
{{#each links}}
<li {{#if active}}class="active"{{/if}}>
<a href="{{url}}">{{name}}</a>
</li>
{{/each}}
</ul>
</nav>
<ul class="gallery">
{{#each gallery.media}}<li>
{{#if media.video}}
<a href="{{url}}"
type="video/mp4"
data-poster="{{thumbnail}}">
{{name}}
</a>
{{else}}
<a href="{{url}}">
<img src="{{thumbnail}}"
alt="{{name}}" />
</a>
{{/if}}
</li>{{/each}}
</ul>
<div id="blueimp-gallery" class="blueimp-gallery">
<div class="slides"></div>
<h3 class="title"></h3>
<a class="prev"></a>
<a class="next"></a>
<a class="close">×</a>
<a class="play-pause"></a>
<ol class="indicator"></ol>
</div>
<script src="public/blueimp/js/blueimp-gallery.min.js"></script>
<script src="public/blueimp/js/blueimp-gallery-video.js"></script>
<script src="public/blueimp/js/blueimp-gallery-indicator.js"></script>
</body>
</html>

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{folder.name}}</title>
<link rel="stylesheet" href="../public/blueimp/css/reset.css" />
<link rel="stylesheet" href="../public/{{css}}.css" />
</head>
<body>
<header>
<h1>{{title}}</h1>
<h2>{{subtitle}}</h2>
</header>
<nav>
<ul>
{{#each links}}
<li>
<a href="{{url}}">{{name}}</a>
</li>
{{/each}}
</ul>
</nav>
</body>
</html>
Loading…
Cancel
Save