piccle 0.1.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/NOTES.md +69 -0
  7. data/README.md +175 -0
  8. data/Rakefile +8 -0
  9. data/agpl-3.0.md +660 -0
  10. data/assets/css/default.css +397 -0
  11. data/assets/css/normalize.css +427 -0
  12. data/assets/icons/android-chrome-192x192.png +0 -0
  13. data/assets/icons/android-chrome-512x512.png +0 -0
  14. data/assets/icons/apple-touch-icon.png +0 -0
  15. data/assets/icons/favicon-16x16.png +0 -0
  16. data/assets/icons/favicon-32x32.png +0 -0
  17. data/assets/icons/favicon.ico +0 -0
  18. data/bin/console +14 -0
  19. data/bin/piccle +355 -0
  20. data/bin/setup +8 -0
  21. data/db/migrations/001_create_photos.rb +15 -0
  22. data/db/migrations/002_update_photos.rb +14 -0
  23. data/db/migrations/003_create_keywords_and_join_table.rb +14 -0
  24. data/db/migrations/004_add_focal_length.rb +7 -0
  25. data/db/migrations/005_create_locations.rb +20 -0
  26. data/js-renderer/handlebars.min-v4.7.6.js +29 -0
  27. data/js-renderer/renderer.js +93 -0
  28. data/lib/piccle.rb +52 -0
  29. data/lib/piccle/config.rb +136 -0
  30. data/lib/piccle/database.rb +33 -0
  31. data/lib/piccle/dstk_service.rb +64 -0
  32. data/lib/piccle/extractor.rb +128 -0
  33. data/lib/piccle/js_renderer.rb +37 -0
  34. data/lib/piccle/models/keyword.rb +6 -0
  35. data/lib/piccle/models/location.rb +11 -0
  36. data/lib/piccle/models/photo.rb +211 -0
  37. data/lib/piccle/parser.rb +230 -0
  38. data/lib/piccle/quilt_generator.rb +30 -0
  39. data/lib/piccle/renderer.rb +175 -0
  40. data/lib/piccle/streams.rb +2 -0
  41. data/lib/piccle/streams/base_stream.rb +56 -0
  42. data/lib/piccle/streams/camera_stream.rb +35 -0
  43. data/lib/piccle/streams/date_stream.rb +95 -0
  44. data/lib/piccle/streams/event_stream.rb +73 -0
  45. data/lib/piccle/streams/keyword_stream.rb +24 -0
  46. data/lib/piccle/streams/location_stream.rb +57 -0
  47. data/lib/piccle/template_helpers.rb +79 -0
  48. data/lib/piccle/version.rb +3 -0
  49. data/lib/tasks/development.rake +38 -0
  50. data/piccle.gemspec +43 -0
  51. data/templates/_breadcrumbs.handlebars.slim +16 -0
  52. data/templates/_footer.handlebars.slim +2 -0
  53. data/templates/_header.handlebars.slim +36 -0
  54. data/templates/_navigation.handlebars.slim +16 -0
  55. data/templates/_substream.handlebars.slim +17 -0
  56. data/templates/feed.atom.slim +29 -0
  57. data/templates/index.html.handlebars.slim +36 -0
  58. data/templates/show.html.handlebars.slim +64 -0
  59. metadata +340 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Browse photos by keyword.
4
+ class Piccle::Streams::KeywordStream < Piccle::Streams::BaseStream
5
+ def namespace
6
+ "by-topic"
7
+ end
8
+
9
+ # Standard method called by the parser object. Returns a hash that contains the data to merge for the given photo.
10
+ def data_for(photo)
11
+ result = { namespace => {
12
+ :friendly_name => "By Topic",
13
+ :interesting => true
14
+ }}
15
+ photo.keywords.each do |kw|
16
+ result[namespace][slugify(kw.name)] = { friendly_name: kw.name, interesting: true, photos: [photo.md5] }
17
+ end
18
+ result
19
+ end
20
+
21
+ def metadata_for(photo)
22
+ photo.keywords.map { |kw| { friendly_name: kw.name, type: :keyword, selector: [namespace, slugify(kw.name)] } }
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Enables browsing photos by location.
4
+ class Piccle::Streams::LocationStream < Piccle::Streams::BaseStream
5
+ def namespace
6
+ "by-location"
7
+ end
8
+
9
+ def data_for(photo)
10
+ data = {}
11
+ if photo.country
12
+ data = { namespace => {
13
+ :friendly_name => "By Location",
14
+ :interesting => false,
15
+ photo.country.downcase => {
16
+ :friendly_name => photo.country,
17
+ :interesting => true,
18
+ :photos => [photo.md5]
19
+ },
20
+ }}
21
+ if photo.state
22
+ data[namespace][photo.country.downcase][photo.state.downcase] = {
23
+ :friendly_name => photo.state,
24
+ :interesting => false,
25
+ :photos => [photo.md5]
26
+ }
27
+
28
+ if photo.city
29
+ data[namespace][photo.country.downcase][photo.state.downcase][photo.city.downcase] = {
30
+ :friendly_name => photo.city,
31
+ :interesting => false,
32
+ :photos => [photo.md5]
33
+ }
34
+ end
35
+ end
36
+ end
37
+
38
+ data
39
+ end
40
+
41
+ def metadata_for(photo)
42
+ metadata = []
43
+
44
+ if photo.country
45
+ metadata << { friendly_name: photo.country, type: :location_country, selector: [namespace, photo.country.downcase] }
46
+
47
+ if photo.state
48
+ metadata << { friendly_name: photo.state, type: :location_state, selector: [namespace, photo.country.downcase, photo.state.downcase] }
49
+
50
+ if photo.city
51
+ metadata << { friendly_name: photo.city, type: :location_city, selector: [namespace, photo.country.downcase, photo.state.downcase, photo.city.downcase] }
52
+ end
53
+ end
54
+ end
55
+ metadata
56
+ end
57
+ end
@@ -0,0 +1,79 @@
1
+ require 'cgi'
2
+ require 'handlebars'
3
+
4
+ # Rendering functions for templates.
5
+ # * For most files, we use Slim to render a Handlebars template (ie. HTML with embedded Handlebars). Handlebars then
6
+ # generates the final output. This lets us use the same templates for backend and frontend rendering.
7
+ # * Partials don't have variable interpolation, because they're rendered inline into the main template. Their variables
8
+ # will be interpolated when the overall template is rendered.
9
+ # * RSS feeds are generated via Slim only, as we don't need to generate those server-side.
10
+ #
11
+ # We don't cache Handlebars templates, because it actually results in WORSE performance than compiling a fresh instance
12
+ # each time.
13
+
14
+ class Piccle::TemplateHelpers
15
+ @@handlebars = nil
16
+ @@slim_pages = {}
17
+ @@slim_partials = {}
18
+
19
+ # Renders a partial template. Partial templates do NOT have their variables interpolated via Handlebars.
20
+ def self.render_partial(template_name, args = {})
21
+ @@slim_partials[template_name] ||= Tilt['slim'].new { File.read(Piccle.config.gem_root_join("templates/_#{template_name}.handlebars.slim")) }
22
+ @@slim_partials[template_name].render(Object.new, args)
23
+ end
24
+
25
+ # Renders an entire template out
26
+ def self.render(template_name, data = {})
27
+ options = { code_attr_delims: { '(' => ')', '[' => ']'}, attr_list_delims: { '(' => ')', '[' => ']' } }
28
+ @@slim_pages[template_name] ||= Tilt['slim'].new(options) { File.read(Piccle.config.gem_root_join("templates/#{template_name}.html.handlebars.slim")) }.render
29
+ template = handlebars.compile(@@slim_pages[template_name])
30
+ template.call(data)
31
+ end
32
+
33
+ def self.render_rss(template_name, data = {})
34
+ @@slim_pages["rss_#{template_name}"] ||= Tilt['slim'].new { File.read(Piccle.config.gem_root_join("templates/#{template_name}.atom.slim")) }
35
+ @@slim_pages["rss_#{template_name}"].render(Object.new, data)
36
+ end
37
+
38
+ # Gets a Handlebars version of the template. No variable replacement!
39
+ def self.compile_template(name)
40
+ slim_template = Tilt['slim'].new { File.read(Piccle.config.gem_root_join("templates/#{name}.html.handlebars.slim")) }
41
+ slim_template.render(Object.new, {})
42
+ end
43
+
44
+ # Given a "selector" (an array of string path components), returns an "include prefix" (a relative path that
45
+ # gets us back to the top level).
46
+ # eg. ["by-date", "2017", "03"] → "../../../"
47
+ def self.include_prefix(selector)
48
+ if selector.any?
49
+ "#{(['..'] * selector.length).join('/')}/"
50
+ else
51
+ ""
52
+ end
53
+ end
54
+
55
+ # Given a block of content, escape its HTML.
56
+ def self.escape_html(&block)
57
+ CGI::escape_html(yield)
58
+ end
59
+
60
+ protected
61
+
62
+ def self.handlebars
63
+ unless @@handlebars
64
+ @@handlebars = Handlebars::Context.new
65
+ @@handlebars.register_helper(:ifEqual) do |context, arg1, arg2, block|
66
+ if arg1 == arg2
67
+ block.fn(context)
68
+ else
69
+ block.inverse(context)
70
+ end
71
+ end
72
+
73
+ @@handlebars.register_helper(:join) do |context, arg1, arg2, block|
74
+ arg1.join(arg2)
75
+ end
76
+ end
77
+ @@handlebars
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module Piccle
2
+ VERSION = "0.1.0.rc1"
3
+ end
@@ -0,0 +1,38 @@
1
+ require "sqlite3"
2
+
3
+ namespace :db do
4
+ desc "Create a database for photo data"
5
+ task :initialise do
6
+ puts "This task is deprecated; run sequel -m db/migrations sqlite://photo_data.db instead."
7
+ # # If a DB already exists, do nothing.
8
+ # if File.exist?(Piccle::Database::PHOTO_DATABASE_FILENAME)
9
+ # puts "Database #{Piccle::Database::PHOTO_DATABASE_FILENAME} already exists; exiting."
10
+ # else
11
+ # puts "Creating an empty DB..."
12
+ # db = SQLite3::Database.new(Piccle::Database::PHOTO_DATABASE_FILENAME)
13
+ # puts " ... and writing a schema to it..."
14
+ # db.execute <<-SQL
15
+ # CREATE TABLE photos(
16
+ # id INTEGER PRIMARY KEY,
17
+ # file_name text NOT NULL,
18
+ # path text NOT NULL,
19
+ # md5 varchar(128) NOT NULL,
20
+ # width integer NOT NULL,
21
+ # height integer NOT NULL,
22
+ # camera_name text,
23
+ # taken_at datetime,
24
+ # created_at datetime
25
+ # );
26
+ # SQL
27
+ # puts " ... Done."
28
+ # end
29
+ end
30
+
31
+ desc "Drop the database"
32
+ task :drop do
33
+ File.delete("photo_data.db")
34
+ end
35
+
36
+ desc "Recreate the database"
37
+ task recreate: [:drop, :initialise]
38
+ end
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'piccle/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "piccle"
8
+ spec.version = Piccle::VERSION
9
+ spec.authors = ["Alex Pounds"]
10
+ spec.email = ["piccle@alexpounds.com"]
11
+ spec.executables = ["piccle"]
12
+
13
+ spec.summary = "A static site generator for photographers"
14
+ spec.description = "Piccle uses the EXIF data present in your photographs and uses it to build a website that lets
15
+ visitors browse by camera, tag, location, and more."
16
+ spec.homepage = "https://piccle.alexpounds.com/"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "bin"
22
+ spec.executables = ["piccle"]
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.13"
26
+ spec.add_development_dependency "rspec", "~> 3.0" # Testing library
27
+ spec.add_development_dependency "simplecov" # Code coverage calculator
28
+ spec.add_development_dependency "simplecov-console" # Output stats on the console
29
+ spec.add_development_dependency "pry-byebug", "~> 3.5" # Debugging aid; only needed by developers.
30
+ spec.add_development_dependency "ruby-prof" # Performance profiling.
31
+
32
+ spec.add_dependency "exifr", "~> 1.3" # EXIF reading library
33
+ spec.add_dependency "handlebars" # Templating engine, usable both backend and frontend.
34
+ spec.add_dependency "httparty" # Simple HTTP library
35
+ spec.add_dependency "rake" # Ruby task runner
36
+ spec.add_dependency "recursive-open-struct", "~> 1.0" # Blesses database results into objects compatible with flavour-saver
37
+ spec.add_dependency "rmagick", "~> 2.0" # Image processing library
38
+ spec.add_dependency "sequel", "~> 5" # DB access in a structured way
39
+ spec.add_dependency "slim", "~> 3.0" # Templating language, so we don't have to write longhand HTML
40
+ spec.add_dependency "sqlite3", "~> 1.3" # Simple file-based database
41
+ spec.add_dependency "thor" # Nice Ruby CLI builder
42
+ spec.add_dependency "xmp", "~> 0.2" # Read XMP info from files
43
+ end
@@ -0,0 +1,16 @@
1
+ |
2
+ {{#if breadcrumbs}}
3
+ <ol>
4
+ {{#each breadcrumbs as |crumb|}}
5
+ {{#if @last}}
6
+ <li>{{crumb.friendly_name}}</li>
7
+ {{else}}
8
+ {{#if crumb.link}}
9
+ <li><a href="{{../include_prefix}}{{crumb.link}}">{{crumb.friendly_name}}</a></li>
10
+ {{else}}
11
+ <li>{{crumb.friendly_name}}</li>
12
+ {{/if}}
13
+ {{/if}}
14
+ {{/each}}
15
+ </ol>
16
+ {{/if}}
@@ -0,0 +1,2 @@
1
+ footer
2
+ | Generated by <a href="https://piccle.alexpounds.com/">Piccle</a>. Photography &copy; {{site_metadata.copyright_year}} {{site_metadata.author_name}}.
@@ -0,0 +1,36 @@
1
+ head
2
+ meta charset="utf-8"
3
+ meta name="viewport" content="width=device-width, initial-scale=1"
4
+ title Photography by {{site_metadata.author_name}}
5
+ link rel="stylesheet" href="{{include_prefix}}css/normalize.css"
6
+ link rel="stylesheet" href="{{include_prefix}}css/default.css"
7
+ link rel="icon" href="{{include_prefix}}icons/favicon.ico"
8
+ link rel="icon" sizes="32x32" type="image/png" href="{{include_prefix}}icons/favicon-32x32.png"
9
+ link rel="icon" sizes="16x16" type="image/png" href="{{include_prefix}}icons/favicon-16x16.png"
10
+ link rel="icon" sizes="192x192" type="image/png" href="{{include_prefix}}icons/android-chrome-192x192.png"
11
+ link rel="icon" sizes="512x512" type="image/png" href="{{include_prefix}}icons/android-chrome-512x512.png"
12
+ link rel="apple-touch-icon" sizes="180x180" type="image/png" href="{{include_prefix}}icons/apple-touch-icon.png"
13
+ |
14
+ {{#if canonical}}
15
+ <link rel="canonical" href="{{include_prefix}}{{canonical}}" />
16
+ {{/if}}
17
+ {{#if prev_link}}
18
+ <link rel="prev" href="{{include_prefix}}{{prev_link}}" />
19
+ {{/if}}
20
+ {{#if next_link}}
21
+ <link rel="next" href="{{include_prefix}}{{next_link}}" />
22
+ {{/if}}
23
+ {{#if site_url}}
24
+ <link rel="alternate" type="application/atom+xml" href="{{include_prefix}}/feed.atom" title="All photos">
25
+ {{/if}}
26
+ {{#if open_graph}}
27
+ <meta property="og:type" content="website" />
28
+ <meta property="og:title" content="{{open_graph.title}}" />
29
+ <meta property="og:description" content="{{open_graph.description}}" />
30
+ <meta property="og:image" content="{{open_graph.image.url}}" />
31
+ <meta property="og:image:type" content="image/jpeg" />
32
+ {{#if open_graph.image.alt}}<meta property="og:image:alt" content="{{open_graph.image.alt}}" />{{/if}}
33
+ <meta property="og:image:width" content="{{open_graph.image.width}}" />
34
+ <meta property="og:image:height" content="{{open_graph.image.height}}" />
35
+ <meta property="og:url" content="{{open_graph.url}}" />
36
+ {{/if}}
@@ -0,0 +1,16 @@
1
+ nav
2
+ |
3
+ <input type="checkbox" id="menu_toggle" name="menu_toggle" />
4
+ <label for="menu_toggle">Menu</label>
5
+ {{#each nav_items as |nav_item|}}
6
+ {{#if nav_item.entries}}
7
+ <section>
8
+ <h2>{{nav_item.friendly_name}}</h2>
9
+ <ul>
10
+ {{#each nav_item.entries as |entry|}}
11
+ <li><a href="{{../../include_prefix}}{{entry.link}}" title="{{entry.photo_count}} {{#ifEqual entry.photo_count 1}}photo{{else}}photos{{/ifEqual}}">{{entry.friendly_name}}</a></li>
12
+ {{/each}}
13
+ </ul>
14
+ </section>
15
+ {{/if}}
16
+ {{/each}}
@@ -0,0 +1,17 @@
1
+ |
2
+ {{#if stream.previous}}
3
+ <a class="navigation_arrow left" href="{{../include_prefix}}{{stream.selector_path}}{{stream.previous.hash}}.html#photo">&laquo;</a>
4
+ {{/if}}
5
+ <div class="stamps">
6
+ {{#each stream.photos as |substream_photo|}}
7
+ <a href="{{../../include_prefix}}{{stream.selector_path}}{{substream_photo.hash}}.html#photo">
8
+ <img
9
+ class="stamp{{#ifEqual substream_photo.hash ../../photo.hash}} current{{/ifEqual}}"
10
+ src="{{../../include_prefix}}images/thumbnails/{{substream_photo.hash}}.{{substream_photo.file_name}}"
11
+ alt="{{substream_photo.title}}" />
12
+ </a>
13
+ {{/each}}
14
+ </div>
15
+ {{#if stream.next}}
16
+ <a class="navigation_arrow right" href="{{../include_prefix}}{{stream.selector_path}}{{stream.next.hash}}.html#photo">&raquo;</a>
17
+ {{/if}}
@@ -0,0 +1,29 @@
1
+ doctype xml
2
+ feed xmlns="http://www.w3.org/2005/Atom"
3
+ title Photography by #{site_metadata[:author_name]}
4
+ author
5
+ name #{site_metadata[:author_name]}
6
+ link rel="self" href=URI::join(Piccle.config.home_url, joined_selector, "feed.atom")
7
+ link rel="alternate" href=URI::join(Piccle.config.home_url, joined_selector, "index.html")
8
+ icon = URI::join(Piccle.config.home_url, "icons", "android-chrome-192x192.png")
9
+ updated = feed_update_time.strftime("%FT%T%:z")
10
+ id = URI::join(Piccle.config.home_url, joined_selector, "feed.atom")
11
+
12
+ - photos.map do |hash, photo|
13
+ entry
14
+ title = photo[:title] || "A photo by #{site_metadata[:author_name]}"
15
+ link rel="alternate" href=URI::join(Piccle.config.home_url, joined_selector, "#{photo[:hash]}.html")
16
+ id = URI::join(Piccle.config.home_url, joined_selector, "#{photo[:hash]}.html")
17
+ updated = photo[:created_at].strftime("%FT%T%:z")
18
+ content type="html"
19
+ == Piccle::TemplateHelpers.escape_html
20
+ - if photo[:title]
21
+ h1 = photo[:title]
22
+ img src=URI::join(Piccle.config.home_url, "/images/photos/", "#{photo[:hash]}.#{URI::DEFAULT_PARSER.escape(photo[:file_name])}")
23
+ - if photo[:description]
24
+ p = photo[:description]
25
+ - if photo[:has_location]
26
+ p #{[photo[:city], photo[:state], photo[:country]].compact.join(", ")}.
27
+ - if photo[:taken_at]
28
+ p Taken #{photo[:taken_at].strftime("%-d %B, %Y.")}
29
+
@@ -0,0 +1,36 @@
1
+ doctype html
2
+ html
3
+ == Piccle::TemplateHelpers.render_partial "header"
4
+
5
+ body.photos_index
6
+ header
7
+ |
8
+ {{#if selector}}
9
+ <h1><a href="{{include_prefix}}index.html">Photography by {{site_metadata.author_name}}</a></h1>
10
+ {{else}}
11
+ <h1>Photography by {{site_metadata.author_name}}</a></h1>
12
+ {{/if}}
13
+ == Piccle::TemplateHelpers.render_partial "breadcrumbs"
14
+ main
15
+ == Piccle::TemplateHelpers.render_partial "navigation"
16
+
17
+ .photos
18
+ |
19
+ {{#each photos as |photo|}}
20
+ {{#if (lookup ../event_ends photo.hash)}}
21
+ {{#with (lookup ../event_ends hash)}}
22
+ <p class="event_block event_end"><a href="{{../include_prefix}}{{join selector '/'}}/index.html">{{name}}</a></p>
23
+ {{/with}}
24
+ {{/if}}
25
+ <a href="{{../include_prefix}}{{../selector_path}}{{photo.hash}}.html#photo">
26
+ <img class="thumbnail" src="{{../include_prefix}}images/thumbnails/{{photo.hash}}.{{photo.file_name}}" alt="{{photo.title}}" />
27
+ </a>
28
+ {{#if (lookup ../event_starts photo.hash)}}
29
+ {{#with (lookup ../event_starts hash)}}
30
+ <p class="event_block event_start"><a href="{{../include_prefix}}{{join selector '/'}}/index.html">{{name}}</a></p>
31
+ {{/with}}
32
+ {{/if}}
33
+ {{/each}}
34
+
35
+ == Piccle::TemplateHelpers.render_partial "footer"
36
+
@@ -0,0 +1,64 @@
1
+ == Piccle::TemplateHelpers.render_partial "header"
2
+
3
+ body.photo_show
4
+ header
5
+ h1
6
+ a href="{{include_prefix}}index.html" Photography by {{site_metadata.author_name}}
7
+ == Piccle::TemplateHelpers.render_partial "breadcrumbs"
8
+ main#photo
9
+ h2
10
+ | {{photo.title}}
11
+ .photo_with_pagination
12
+ | {{#if prev_link}}
13
+ <a class="navigation_arrow" href="{{include_prefix}}{{prev_link}}#photo">&laquo;</a>
14
+ {{else}}
15
+ <span class="navigation_arrow">&nbsp;</span>
16
+ {{/if}}
17
+ img src="{{include_prefix}}images/photos/{{photo.hash}}.{{photo.file_name}}"
18
+ | {{#if next_link}}
19
+ <a class="navigation_arrow" href="{{include_prefix}}{{next_link}}#photo">&raquo;</a>
20
+ {{else}}
21
+ <span class="navigation_arrow">&nbsp;</span>
22
+ {{/if}}
23
+
24
+ p.description
25
+ | {{photo.description}}
26
+ p.settings
27
+ | {{#if camera_link}}
28
+ <a href="{{include_prefix}}{{camera_link.link}}">{{camera_link.friendly_name}}</a>{{/if}}{{#if photo.focal_length}}, {{photo.focal_length}}{{/if}}{{#if photo.aperture}}, f/{{photo.aperture}}{{/if}}{{#if photo.shutter_speed}}, {{photo.shutter_speed}}{{/if}}{{#if photo.iso}}, ISO {{photo.iso}}{{/if}}.
29
+ | {{#if photo.taken_at}}
30
+ <p class="date">
31
+ {{#if day_link}}<a href="{{include_prefix}}{{day_link.link}}">{{day_link.friendly_name}}</a>{{/if}}
32
+ {{#if month_link}}<a href="{{include_prefix}}{{month_link.link}}">{{month_link.friendly_name}}</a>, {{/if}}
33
+ {{#if year_link}}<a href="{{include_prefix}}{{year_link.link}}">{{year_link.friendly_name}}</a>.{{/if}}
34
+ </p>
35
+ {{/if}}
36
+ {{#if photo.has_location}}
37
+ <ul class="location">
38
+ {{#if city_link}}<li><a href="{{include_prefix}}{{city_link.link}}">{{city_link.friendly_name}}</a></li>{{/if}}
39
+ {{#if state_link}}<li><a href="{{include_prefix}}{{state_link.link}}">{{state_link.friendly_name}}</a></li>{{/if}}
40
+ {{#if country_link}}<li><a href="{{include_prefix}}{{country_link.link}}">{{country_link.friendly_name}}</a></li>{{/if}}
41
+ </ul>
42
+ {{/if}}
43
+
44
+ | {{#if keywords}}
45
+ <div class="keywords">
46
+ {{#each keywords as |keyword|}}
47
+ <a href="{{../include_prefix}}{{keyword.link}}">{{keyword.friendly_name}}</a>
48
+ {{/each}}
49
+ </div>
50
+ {{/if}}
51
+
52
+ .streams
53
+ |
54
+ {{#each substreams as |stream|}}
55
+ <section>
56
+ <h2>{{stream.title}}</h2>
57
+ <div class="stream">
58
+ == Piccle::TemplateHelpers.render_partial "substream"
59
+ |
60
+ </div>
61
+ </section>
62
+ {{/each}}
63
+
64
+ == Piccle::TemplateHelpers.render_partial "footer"