Browse Source

Initial version with PhotoSwipe library

Innocenty Enikeew 10 years ago
parent
commit
f2758abde7

+ 3 - 1
.gitignore

@@ -1,2 +1,4 @@
 *~
-*.fasl
+*.fasl
+config.lisp
+node_modules/

+ 7 - 1
README.md

@@ -1,7 +1,7 @@
 Features
 ========
 
-  * File system layout (YEAR > YEAR-MONTH > YEAR-MONTH-DAY > filename)
+  * File system layout (YEAR > YEAR-MONTH-ALBUM > filename)
   * Image database (timeline, albums, album tags, image tags)
   * Catalog browse
   * Auto-move
@@ -9,3 +9,9 @@ Features
   * Geo-tagging and geo-browse
   * Clustering single photos (multiple takes of the same)
   * Clustring albums from input
+
+Database
+
+photos: id | album_id | filename | size | taken | width | height | lat | lon
+albums: id | parent_id | path | date | name | description | cover_id | accessibility
+album_items: album_id | photo_id | idx | description

+ 483 - 0
assets/default-skin.css

@@ -0,0 +1,483 @@
+/*! PhotoSwipe Default UI CSS by Dmitry Semenov | photoswipe.com | MIT license */
+/*
+
+	Contents:
+
+	1. Buttons
+	2. Share modal and links
+	3. Index indicator ("1 of X" counter)
+	4. Caption
+	5. Loading indicator
+	6. Additional styles (root element, top bar, idle state, hidden state, etc.)
+
+*/
+/*
+	
+	1. Buttons
+
+ */
+/* <button> css reset */
+.pswp__button {
+  width: 44px;
+  height: 44px;
+  position: relative;
+  background: none;
+  cursor: pointer;
+  overflow: visible;
+  -webkit-appearance: none;
+  display: block;
+  border: 0;
+  padding: 0;
+  margin: 0;
+  float: right;
+  opacity: 0.75;
+  -webkit-transition: opacity 0.2s;
+          transition: opacity 0.2s;
+  -webkit-box-shadow: none;
+          box-shadow: none; }
+  .pswp__button:focus,
+  .pswp__button:hover {
+    opacity: 1; }
+  .pswp__button:active {
+    outline: none;
+    opacity: 0.9; }
+  .pswp__button::-moz-focus-inner {
+    padding: 0;
+    border: 0; }
+
+/* pswp__ui--over-close class it added when mouse is over element that should close gallery */
+.pswp__ui--over-close .pswp__button--close {
+  opacity: 1; }
+
+.pswp__button,
+.pswp__button--arrow--left:before,
+.pswp__button--arrow--right:before {
+  background: url(default-skin.png) 0 0 no-repeat;
+  background-size: 264px 88px;
+  width: 44px;
+  height: 44px; }
+
+@media (-webkit-min-device-pixel-ratio: 1.1), (-webkit-min-device-pixel-ratio: 1.09375), (min-resolution: 105dpi), (min-resolution: 1.1dppx) {
+  /* Serve SVG sprite if browser supports SVG and resolution is more than 105dpi */
+  .pswp--svg .pswp__button,
+  .pswp--svg .pswp__button--arrow--left:before,
+  .pswp--svg .pswp__button--arrow--right:before {
+    background-image: url(default-skin.svg); }
+  .pswp--svg .pswp__button--arrow--left,
+  .pswp--svg .pswp__button--arrow--right {
+    background: none; } }
+
+.pswp__button--close {
+  background-position: 0 -44px; }
+
+.pswp__button--share {
+  background-position: -44px -44px; }
+
+.pswp__button--fs {
+  display: none; }
+
+.pswp--supports-fs .pswp__button--fs {
+  display: block; }
+
+.pswp--fs .pswp__button--fs {
+  background-position: -44px 0; }
+
+.pswp__button--zoom {
+  display: none;
+  background-position: -88px 0; }
+
+.pswp--zoom-allowed .pswp__button--zoom {
+  display: block; }
+
+.pswp--zoomed-in .pswp__button--zoom {
+  background-position: -132px 0; }
+
+/* no arrows on touch screens */
+.pswp--touch .pswp__button--arrow--left,
+.pswp--touch .pswp__button--arrow--right {
+  visibility: hidden; }
+
+/*
+	Arrow buttons hit area
+	(icon is added to :before pseudo-element)
+*/
+.pswp__button--arrow--left,
+.pswp__button--arrow--right {
+  background: none;
+  top: 50%;
+  margin-top: -50px;
+  width: 70px;
+  height: 100px;
+  position: absolute; }
+
+.pswp__button--arrow--left {
+  left: 0; }
+
+.pswp__button--arrow--right {
+  right: 0; }
+
+.pswp__button--arrow--left:before,
+.pswp__button--arrow--right:before {
+  content: '';
+  top: 35px;
+  background-color: rgba(0, 0, 0, 0.3);
+  height: 30px;
+  width: 32px;
+  position: absolute; }
+
+.pswp__button--arrow--left:before {
+  left: 6px;
+  background-position: -138px -44px; }
+
+.pswp__button--arrow--right:before {
+  right: 6px;
+  background-position: -94px -44px; }
+
+/*
+
+	2. Share modal/popup and links
+
+ */
+.pswp__counter,
+.pswp__share-modal {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+      user-select: none; }
+
+.pswp__share-modal {
+  display: block;
+  background: rgba(0, 0, 0, 0.5);
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  padding: 10px;
+  position: absolute;
+  z-index: 1600;
+  opacity: 0;
+  -webkit-transition: opacity 0.25s ease-out;
+          transition: opacity 0.25s ease-out;
+  -webkit-backface-visibility: hidden;
+  will-change: opacity; }
+
+.pswp__share-modal--hidden {
+  display: none; }
+
+.pswp__share-tooltip {
+  z-index: 1620;
+  position: absolute;
+  background: #FFF;
+  top: 56px;
+  border-radius: 2px;
+  display: block;
+  width: auto;
+  right: 44px;
+  -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
+          box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
+  -webkit-transform: translateY(6px);
+      -ms-transform: translateY(6px);
+          transform: translateY(6px);
+  -webkit-transition: -webkit-transform 0.25s;
+          transition: transform 0.25s;
+  -webkit-backface-visibility: hidden;
+  will-change: transform; }
+  .pswp__share-tooltip a {
+    display: block;
+    padding: 8px 12px;
+    color: #000;
+    text-decoration: none;
+    font-size: 14px;
+    line-height: 18px; }
+    .pswp__share-tooltip a:hover {
+      text-decoration: none;
+      color: #000; }
+    .pswp__share-tooltip a:first-child {
+      /* round corners on the first/last list item */
+      border-radius: 2px 2px 0 0; }
+    .pswp__share-tooltip a:last-child {
+      border-radius: 0 0 2px 2px; }
+
+.pswp__share-modal--fade-in {
+  opacity: 1; }
+  .pswp__share-modal--fade-in .pswp__share-tooltip {
+    -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+            transform: translateY(0); }
+
+/* increase size of share links on touch devices */
+.pswp--touch .pswp__share-tooltip a {
+  padding: 16px 12px; }
+
+a.pswp__share--facebook:before {
+  content: '';
+  display: block;
+  width: 0;
+  height: 0;
+  position: absolute;
+  top: -12px;
+  right: 15px;
+  border: 6px solid transparent;
+  border-bottom-color: #FFF;
+  -webkit-pointer-events: none;
+  -moz-pointer-events: none;
+  pointer-events: none; }
+
+a.pswp__share--facebook:hover {
+  background: #3E5C9A;
+  color: #FFF; }
+  a.pswp__share--facebook:hover:before {
+    border-bottom-color: #3E5C9A; }
+
+a.pswp__share--twitter:hover {
+  background: #55ACEE;
+  color: #FFF; }
+
+a.pswp__share--pinterest:hover {
+  background: #CCC;
+  color: #CE272D; }
+
+a.pswp__share--download:hover {
+  background: #DDD; }
+
+/*
+
+	3. Index indicator ("1 of X" counter)
+
+ */
+.pswp__counter {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 44px;
+  font-size: 13px;
+  line-height: 44px;
+  color: #FFF;
+  opacity: 0.75;
+  padding: 0 10px; }
+
+/*
+	
+	4. Caption
+
+ */
+.pswp__caption {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  min-height: 44px; }
+  .pswp__caption small {
+    font-size: 11px;
+    color: #BBB; }
+
+.pswp__caption__center {
+  text-align: left;
+  max-width: 420px;
+  margin: 0 auto;
+  font-size: 13px;
+  padding: 10px;
+  line-height: 20px;
+  color: #CCC; }
+
+.pswp__caption--empty {
+  display: none; }
+
+/* Fake caption element, used to calculate height of next/prev image */
+.pswp__caption--fake {
+  visibility: hidden; }
+
+/*
+
+	5. Loading indicator (preloader)
+
+	You can play with it here - http://codepen.io/dimsemenov/pen/yyBWoR
+
+ */
+.pswp__preloader {
+  width: 44px;
+  height: 44px;
+  position: absolute;
+  top: 0;
+  left: 50%;
+  margin-left: -22px;
+  opacity: 0;
+  -webkit-transition: opacity 0.25s ease-out;
+          transition: opacity 0.25s ease-out;
+  will-change: opacity;
+  direction: ltr; }
+
+.pswp__preloader__icn {
+  width: 20px;
+  height: 20px;
+  margin: 12px; }
+
+.pswp__preloader--active {
+  opacity: 1; }
+  .pswp__preloader--active .pswp__preloader__icn {
+    /* We use .gif in browsers that don't support CSS animation */
+    background: url(preloader.gif) 0 0 no-repeat; }
+
+.pswp--css_animation .pswp__preloader--active {
+  opacity: 1; }
+  .pswp--css_animation .pswp__preloader--active .pswp__preloader__icn {
+    -webkit-animation: clockwise 500ms linear infinite;
+            animation: clockwise 500ms linear infinite; }
+  .pswp--css_animation .pswp__preloader--active .pswp__preloader__donut {
+    -webkit-animation: donut-rotate 1000ms cubic-bezier(0.4, 0, 0.22, 1) infinite;
+            animation: donut-rotate 1000ms cubic-bezier(0.4, 0, 0.22, 1) infinite; }
+
+.pswp--css_animation .pswp__preloader__icn {
+  background: none;
+  opacity: 0.75;
+  width: 14px;
+  height: 14px;
+  position: absolute;
+  left: 15px;
+  top: 15px;
+  margin: 0; }
+
+.pswp--css_animation .pswp__preloader__cut {
+  /* 
+			The idea of animating inner circle is based on Polymer ("material") loading indicator 
+			 by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html
+		*/
+  position: relative;
+  width: 7px;
+  height: 14px;
+  overflow: hidden; }
+
+.pswp--css_animation .pswp__preloader__donut {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  width: 14px;
+  height: 14px;
+  border: 2px solid #FFF;
+  border-radius: 50%;
+  border-left-color: transparent;
+  border-bottom-color: transparent;
+  position: absolute;
+  top: 0;
+  left: 0;
+  background: none;
+  margin: 0; }
+
+@media screen and (max-width: 1024px) {
+  .pswp__preloader {
+    position: relative;
+    left: auto;
+    top: auto;
+    margin: 0;
+    float: right; } }
+
+@-webkit-keyframes clockwise {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(360deg);
+            transform: rotate(360deg); } }
+
+@keyframes clockwise {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(360deg);
+            transform: rotate(360deg); } }
+
+@-webkit-keyframes donut-rotate {
+  0% {
+    -webkit-transform: rotate(0);
+            transform: rotate(0); }
+  50% {
+    -webkit-transform: rotate(-140deg);
+            transform: rotate(-140deg); }
+  100% {
+    -webkit-transform: rotate(0);
+            transform: rotate(0); } }
+
+@keyframes donut-rotate {
+  0% {
+    -webkit-transform: rotate(0);
+            transform: rotate(0); }
+  50% {
+    -webkit-transform: rotate(-140deg);
+            transform: rotate(-140deg); }
+  100% {
+    -webkit-transform: rotate(0);
+            transform: rotate(0); } }
+
+/*
+	
+	6. Additional styles
+
+ */
+/* root element of UI */
+.pswp__ui {
+  -webkit-font-smoothing: auto;
+  visibility: visible;
+  opacity: 1;
+  z-index: 1550; }
+
+/* top black bar with buttons and "1 of X" indicator */
+.pswp__top-bar {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 44px;
+  width: 100%; }
+
+.pswp__caption,
+.pswp__top-bar,
+.pswp--has_mouse .pswp__button--arrow--left,
+.pswp--has_mouse .pswp__button--arrow--right {
+  -webkit-backface-visibility: hidden;
+  will-change: opacity;
+  -webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
+          transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); }
+
+/* pswp--has_mouse class is added only when two subsequent mousemove events occur */
+.pswp--has_mouse .pswp__button--arrow--left,
+.pswp--has_mouse .pswp__button--arrow--right {
+  visibility: visible; }
+
+.pswp__top-bar,
+.pswp__caption {
+  background-color: rgba(0, 0, 0, 0.5); }
+
+/* pswp__ui--fit class is added when main image "fits" between top bar and bottom bar (caption) */
+.pswp__ui--fit .pswp__top-bar,
+.pswp__ui--fit .pswp__caption {
+  background-color: rgba(0, 0, 0, 0.3); }
+
+/* pswp__ui--idle class is added when mouse isn't moving for several seconds (JS option timeToIdle) */
+.pswp__ui--idle .pswp__top-bar {
+  opacity: 0; }
+
+.pswp__ui--idle .pswp__button--arrow--left,
+.pswp__ui--idle .pswp__button--arrow--right {
+  opacity: 0; }
+
+/*
+	pswp__ui--hidden class is added when controls are hidden
+	e.g. when user taps to toggle visibility of controls
+*/
+.pswp__ui--hidden .pswp__top-bar,
+.pswp__ui--hidden .pswp__caption,
+.pswp__ui--hidden .pswp__button--arrow--left,
+.pswp__ui--hidden .pswp__button--arrow--right {
+  /* Force paint & create composition layer for controls. */
+  opacity: 0.001; }
+
+/* pswp__ui--one-slide class is added when there is just one item in gallery */
+.pswp__ui--one-slide .pswp__button--arrow--left,
+.pswp__ui--one-slide .pswp__button--arrow--right,
+.pswp__ui--one-slide .pswp__counter {
+  display: none; }
+
+.pswp__element--disabled {
+  display: none !important; }
+
+.pswp--minimal--dark .pswp__top-bar {
+  background: none; }

BIN
assets/default-skin.png


+ 1 - 0
assets/default-skin.svg

@@ -0,0 +1 @@
+<svg width="264" height="88" viewBox="0 0 264 88" xmlns="http://www.w3.org/2000/svg"><title>default-skin 2</title><g fill="none" fill-rule="evenodd"><g><path d="M67.002 59.5v3.768c-6.307.84-9.184 5.75-10.002 9.732 2.22-2.83 5.564-5.098 10.002-5.098V71.5L73 65.585 67.002 59.5z" id="Shape" fill="#fff"/><g fill="#fff"><path d="M13 29v-5h2v3h3v2h-5zM13 15h5v2h-3v3h-2v-5zM31 15v5h-2v-3h-3v-2h5zM31 29h-5v-2h3v-3h2v5z" id="Shape"/></g><g fill="#fff"><path d="M62 24v5h-2v-3h-3v-2h5zM62 20h-5v-2h3v-3h2v5zM70 20v-5h2v3h3v2h-5zM70 24h5v2h-3v3h-2v-5z"/></g><path d="M20.586 66l-5.656-5.656 1.414-1.414L22 64.586l5.656-5.656 1.414 1.414L23.414 66l5.656 5.656-1.414 1.414L22 67.414l-5.656 5.656-1.414-1.414L20.586 66z" fill="#fff"/><path d="M111.785 65.03L110 63.5l3-3.5h-10v-2h10l-3-3.5 1.785-1.468L117 59l-5.215 6.03z" fill="#fff"/><path d="M152.215 65.03L154 63.5l-3-3.5h10v-2h-10l3-3.5-1.785-1.468L147 59l5.215 6.03z" fill="#fff"/><g><path id="Rectangle-11" fill="#fff" d="M160.957 28.543l-3.25-3.25-1.413 1.414 3.25 3.25z"/><path d="M152.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" id="Oval-1" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M150 21h5v1h-5z"/></g><g><path d="M116.957 28.543l-1.414 1.414-3.25-3.25 1.414-1.414 3.25 3.25z" fill="#fff"/><path d="M108.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M106 21h5v1h-5z"/><path fill="#fff" d="M109.043 19.008l-.085 5-1-.017.085-5z"/></g></g></g></svg>

File diff suppressed because it is too large
+ 3 - 0
assets/photoswipe-ui-default.min.js


+ 178 - 0
assets/photoswipe.css

@@ -0,0 +1,178 @@
+/*! PhotoSwipe main CSS by Dmitry Semenov | photoswipe.com | MIT license */
+/*
+	Styles for basic PhotoSwipe functionality (sliding area, open/close transitions)
+*/
+/* pswp = photoswipe */
+.pswp {
+  display: none;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  top: 0;
+  overflow: hidden;
+  -ms-touch-action: none;
+  touch-action: none;
+  z-index: 1500;
+  -webkit-text-size-adjust: 100%;
+  /* create separate layer, to avoid paint on window.onscroll in webkit/blink */
+  -webkit-backface-visibility: hidden;
+  outline: none; }
+  .pswp * {
+    -webkit-box-sizing: border-box;
+            box-sizing: border-box; }
+  .pswp img {
+    max-width: none; }
+
+/* style is added when JS option showHideOpacity is set to true */
+.pswp--animate_opacity {
+  /* 0.001, because opacity:0 doesn't trigger Paint action, which causes lag at start of transition */
+  opacity: 0.001;
+  will-change: opacity;
+  /* for open/close transition */
+  -webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
+          transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); }
+
+.pswp--open {
+  display: block; }
+
+.pswp--zoom-allowed .pswp__img {
+  /* autoprefixer: off */
+  cursor: -webkit-zoom-in;
+  cursor: -moz-zoom-in;
+  cursor: zoom-in; }
+
+.pswp--zoomed-in .pswp__img {
+  /* autoprefixer: off */
+  cursor: -webkit-grab;
+  cursor: -moz-grab;
+  cursor: grab; }
+
+.pswp--dragging .pswp__img {
+  /* autoprefixer: off */
+  cursor: -webkit-grabbing;
+  cursor: -moz-grabbing;
+  cursor: grabbing; }
+
+/*
+	Background is added as a separate element.
+	As animating opacity is much faster than animating rgba() background-color.
+*/
+.pswp__bg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background: #000;
+  opacity: 0;
+  -webkit-backface-visibility: hidden;
+  will-change: opacity; }
+
+.pswp__scroll-wrap {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden; }
+
+.pswp__container,
+.pswp__zoom-wrap {
+  -ms-touch-action: none;
+  touch-action: none;
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0; }
+
+/* Prevent selection and tap highlights */
+.pswp__container,
+.pswp__img {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+      user-select: none;
+  -webkit-tap-highlight-color: transparent;
+  -webkit-touch-callout: none; }
+
+.pswp__zoom-wrap {
+  position: absolute;
+  width: 100%;
+  -webkit-transform-origin: left top;
+  -ms-transform-origin: left top;
+  transform-origin: left top;
+  /* for open/close transition */
+  -webkit-transition: -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1);
+          transition: transform 333ms cubic-bezier(0.4, 0, 0.22, 1); }
+
+.pswp__bg {
+  will-change: opacity;
+  /* for open/close transition */
+  -webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
+          transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); }
+
+.pswp--animated-in .pswp__bg,
+.pswp--animated-in .pswp__zoom-wrap {
+  -webkit-transition: none;
+  transition: none; }
+
+.pswp__container,
+.pswp__zoom-wrap {
+  -webkit-backface-visibility: hidden;
+  will-change: transform; }
+
+.pswp__item {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  overflow: hidden; }
+
+.pswp__img {
+  position: absolute;
+  width: auto;
+  height: auto;
+  top: 0;
+  left: 0; }
+
+/*
+	stretched thumbnail or div placeholder element (see below)
+	style is added to avoid flickering in webkit/blink when layers overlap
+*/
+.pswp__img--placeholder {
+  -webkit-backface-visibility: hidden; }
+
+/*
+	div element that matches size of large image
+	large image loads on top of it
+*/
+.pswp__img--placeholder--blank {
+  background: #222; }
+
+.pswp--ie .pswp__img {
+  width: 100% !important;
+  height: auto !important;
+  left: 0;
+  top: 0; }
+
+/*
+	Error message appears when image is not loaded
+	(JS option errorMsg controls markup)
+*/
+.pswp__error-msg {
+  position: absolute;
+  left: 0;
+  top: 50%;
+  width: 100%;
+  text-align: center;
+  font-size: 14px;
+  line-height: 16px;
+  margin-top: -8px;
+  color: #CCC; }
+
+.pswp__error-msg a {
+  color: #CCC;
+  text-decoration: underline; }

File diff suppressed because it is too large
+ 3 - 0
assets/photoswipe.min.js


BIN
assets/preloader.gif


+ 712 - 0
assets/site.css

@@ -0,0 +1,712 @@
+/* Demo site CSS. Not mobile first, not semantic, not optimized, made for 20 minutes, mess */
+html, body, div, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, ol, ul, li, form, fieldset, legend, label, table, header, footer, nav, section, figure {
+  margin: 0;
+  padding: 0; 
+}
+figure {
+  display: block;
+}
+html {
+  /*overflow-y: scroll;*/
+}
+
+a {
+  color: #3169B3;
+}
+a:hover {
+  color: #C00;
+}
+
+* {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+body {
+  font-family: "myriad-pro","Myriad Pro","Helvetica Neue",Helvetica,Arial,sans-serif;
+  font-size: 18px;
+  line-height: 26px;
+  color: #282B30;
+
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -moz-font-feature-settings: "liga", "kern";
+
+}
+
+
+
+/*code {
+  background: #F8F8F8;
+  padding: .1em .4em;
+  color: #c82829;
+  font-size: 14px;
+}*/
+code, pre {
+  font-family:  Menlo, Monaco, monospace;
+  font-size: 12px;
+  line-height: 1.45;
+  color: #333;
+  tab-size: 4;
+}
+pre {
+  padding: 0;
+  margin: 0;
+  overflow: auto;
+  
+  word-wrap: normal;
+
+  text-rendering: auto;
+  -webkit-font-smoothing: auto;
+}
+
+/*body.open-sans {
+  font-family: "open-sans";
+}
+.futura-pt {
+
+}
+.futura-pt p {
+  font-family: "adelle";
+}
+.futura-pt h1, .futura-pt h2 {
+  font-family: "futura-pt";
+  text-transform: uppercase;
+}
+.futura-pt h3 {
+  font-weight: bold;
+}
+*/
+
+
+img {
+  width: auto;
+  max-width: 100%;
+  height: auto;
+  border: 0;
+}
+
+
+.video {
+  width: 100%%;
+  margin: 0 0 24px 0;
+}
+.video__container {
+  position: relative;
+  width: 100%;
+  padding-bottom: 56.25%;
+  padding-top: 35px;
+  height: 0;
+  overflow: hidden;
+}
+.video__container iframe {
+  position: absolute;
+  top:0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+.video-desc {
+  width: 100%;
+  max-width: 740px;
+  margin: 12px auto;
+}
+
+
+p {
+  margin: 0 0 12px;
+}
+ul {
+  list-style: disc;
+}
+ul, ol {
+  padding: 0;
+  margin: 0 0 12px 25px;
+}
+li {
+  margin: 0 0 12px 0;
+}
+h1, h2, h3, h4, h5, h6 {
+  margin: 0;
+  font-weight: normal;
+}
+
+
+h1 {
+  font-size: 48px;
+  line-height: 1;
+  margin: 0;
+  font-weight: 600;
+  margin-bottom: 6px;
+}
+
+.row--heading {
+  position: relative;
+}
+.section--head p {
+  text-align: left;
+  font-size: 26px;
+  line-height: 32px;
+  margin-bottom: 24px;
+
+   font-size: 19px;
+  line-height: 29px;
+  
+}
+
+
+.row--nav {
+  text-align: center;
+  font-size: 32px;
+  line-height: 1.1; 
+}
+.navigation {
+  width: 100%;
+  background: #F7F7F7;
+  position: relative;
+  padding: 30px 0;
+}
+.navigation a {
+  text-decoration: none;
+  border-bottom: 1px solid #92ABCD;
+}
+
+
+h2 {
+  font-size: 32px;
+  line-height: 1.1;
+  margin-bottom: 12px;
+  font-weight: 600;
+}
+
+h3 {
+  font-size: 22px;
+  line-height: 28px;
+  margin: 0 0 8px 0;
+
+  font-size: 18px;
+  line-height: 24px;
+  font-weight: 600;
+}
+strong {
+  font-weight: 600;
+}
+span.highlight {
+  background:rgb(246, 243, 226);
+}
+.title-block p {
+    font-size: 22px;
+  line-height: 28px;
+  max-width: 600px;
+  margin-bottom: 24px;
+
+}
+
+p, ul.text-list {
+  color: #444;
+}
+.section--head p,
+.title-block p {
+  color: #666;
+}
+
+
+
+
+.title-block p {
+  font-size: 19px;
+  line-height: 29px;
+}
+
+
+
+
+
+.section {
+  width: 100%;
+  margin-top: 176px;
+}
+.docs {
+  margin-top: 60px;
+}
+.section--head {
+  margin: 0;
+  background: #EEE;
+  /*background: linear-gradient(180deg, #eee 0%,#fff 100%);*/
+  padding: 5px 0;
+}
+
+
+.row {
+  max-width: 800px;
+  margin: 24px auto;
+  padding: 0 30px;
+  position: relative;
+}
+
+.row--docs {
+  max-width: 960px;
+  padding-left: 210px;
+}
+.row--heading {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+.row--footer {
+  text-align: center;
+  padding: 132px 0;
+}
+.row--wide {
+  max-width: 1000px;
+}
+.row--video {
+  max-width: 1160px;
+}
+
+.row--wide img {
+  float: left;
+  margin: 0 12px 12px 0;
+  width: 200px;
+}
+img.img--with_border {
+  border: 1px solid #DDD;
+  border-radius: 2px;
+}
+.img-desc {
+  margin-left: 212px;
+}
+
+
+
+
+/*.main-wrapper {
+  background: none;
+	width: 100%;
+  padding-top: 0;
+  
+}
+.content-wrap {
+  
+
+
+}*/
+
+
+/* clearfix */
+.row:after{
+  content: " "; 
+   visibility: hidden;
+   display: block;
+   height: 0;
+   clear: both;
+}
+.row--large {
+  margin-bottom: 48px;
+}
+
+
+.section--head h1 a {
+  vertical-align: super;
+  font-size: 14px;
+  text-decoration: none;
+}
+
+/*p.intro {
+  margin-top: 24px;
+  font-size: 18px;
+  line-height: 24px;
+}*/
+
+
+
+
+
+
+
+.col-50 {
+  width: 48%;
+  float: left;
+}
+.col-50:nth-child(1) {
+  margin-right: 4%;
+}
+.col-img {
+  width: auto;
+  max-width: 100%;
+  height: auto;
+}
+
+.style-select {
+  width: 100%;
+  float: left;
+}
+.style-select .radio {
+  position: relative;
+  margin-bottom: 12px;
+  display: block;
+  float: left;
+  width: 100%;
+}
+.style-select label {
+  padding-left: 24px;
+  position: relative;
+  display: block;
+  cursor: pointer;
+}
+.style-select input {
+  position: absolute;
+  left:0;
+  top:0;
+  width:24px;
+  height:24px;
+  overflow:hidden;
+  margin:0;
+  padding:0;
+  border:0;
+  outline:0;
+  opacity:0;
+  cursor: pointer;
+  
+}
+.style-select input + label:before {
+  content: '';
+  position: absolute;
+  left:0;
+  top:4px;
+  background: none;
+  border-radius: 50%;
+  width:16px;
+  height:16px;
+  box-sizing: border-box;
+  border: 2px solid rgba(0,0,0,0.6);
+}
+.radio:hover label:before {
+  border-color: #3169B3;
+}
+.style-select input:checked + label:before {
+  border-color: #3169B3;
+}
+.style-select input:checked + label:after {
+  content: '';
+  position: absolute;
+  left: 5px;
+  top: 9px;
+  width:6px;
+  height: 6px;
+  background: #3169B3;
+  border-radius: 50%;
+}
+
+/*.section--developers {
+  color: #FFF;
+  background: #222;
+  background: #1f1f1f;
+  padding: 80px 0;
+}
+.section--developers a {
+  color:#FFFFFF;
+}
+.section--developers p,
+.section--developers  ul.text-list {
+  color: #ddd;
+}
+.section--developers .section--head p,
+.section--developers .title-block p {
+  color: #ddd;
+}*/
+
+.row--modules ul {
+  list-style: none;
+  margin-left: 0;
+  /*color: rgba(255, 255, 255, 0.8);*/
+}
+.row--modules span {
+  font-wight:600;
+  border-left:5px solid #FFF;
+  padding-left:5px;
+  margin-left:-10px;
+}
+
+.size-chart {
+  width: 100%;
+  display: inline-block;
+}
+
+.size-chart div {
+  height: 21px;
+  float: left;
+  font-size: 13px;
+  padding: 4px;
+  line-height: 1;
+}
+
+.block__ui-separated {
+  position: relative;
+}
+.block__ui-separated .col-50 {
+  position: absolute;
+  left: 0;
+  top:12px;
+}
+.block__ui-separated img {
+  float: right;
+  max-width: 450px;
+}
+
+
+.demo-gallery {
+  width: 100%;
+  height: auto;
+  float: left;
+}
+.demo-gallery a {
+  -webkit-tap-highlight-color: rgba(0,0,0,0);
+  -webkit-touch-callout: none;
+  display: block;
+  float: left;
+  margin: 0 12px 12px 0;
+  width: 171px;
+  line-height: 0;
+}
+
+
+
+a.demo-gallery__img--main {
+  width: auto;
+  height: auto;
+}
+
+.ukraine-flag {
+  width: 21px;
+  height: 14px;
+  position: relative;
+  background: #ffcc00;
+  top: 1px;
+  display: inline-block;
+}
+.ukraine-flag:before {
+ content:'';
+ position: absolute;
+ width: 21px;
+ height: 7px;
+ left:0;
+ top:0;
+ background: #0066cc;
+}
+
+.demo-gallery figure {
+  display: none;
+}
+.demo-gallery__title {
+  line-height: 14px;
+  font-size: 14px;
+  opacity: 0.8;
+  margin-top: 5px;
+  width: 100%;
+  float: left;
+}
+
+.share-buttons h2 {
+  text-align: center;
+  border: 0;
+  
+}
+.share-buttons {
+  text-align: center;
+  position: relative;
+  margin: 0 0 24px;
+}
+.share-buttons a {
+  -moz-border-radius: 2px;
+  border-radius: 2px;
+  display: inline-block;
+  padding: 10px 20px;
+  margin: 10px;
+  color: #FFF;
+  text-decoration: none;
+  background: #5AAF63;
+  font-size: 16px;
+  line-height: 22px;
+  cursor: pointer;
+}
+.share-buttons a:hover {
+  opacity: 0.7;
+}
+#tweet {
+  background: #0096c4;
+}
+#like {
+  background: #3b5998;
+}
+#gplus {
+  background: #d34836;
+}
+
+
+
+@media screen and (max-width: 1000px) {
+
+ 
+
+  .row--wide {
+    max-width: 800px;
+  }
+  .row--wide img {
+    float: none;
+  }
+  .img-desc {
+    margin-left: 0;
+  }
+  .section {
+    margin-top: 132px;
+  }
+
+  .row--docs {
+    max-width: 800px;
+    padding-left: 30px;
+
+  }
+  
+  .section--head {
+    margin-top:0;
+    padding: 30px 0;
+  }
+
+  .docs {
+    margin-top: 48px;
+  }
+
+  .docs-menu {
+    position: relative;
+    left: 0;
+    top: 0;
+    border-top: 1px solid #CCC;
+    padding-top: 25px;
+    border-bottom: 1px solid #CCC;
+    padding-bottom: 25px;
+    margin: 15px 0;
+  }
+
+  .row--nav {
+    font-size: 24px;
+    line-height: 1.1; 
+  }
+
+}
+
+@media screen and (max-width: 650px) {
+   h1 {
+    font-size: 40px;
+  }
+  .block__ui-separated .col-50 {
+    position: relative;
+    top: 0;
+  }
+  .block__ui-separated img {
+    max-width: 100%;
+    width: 100%;
+    float: left;
+  }
+}
+
+@media screen and (max-width: 450px) {
+  
+
+  .col-50:nth-child(1) {
+    margin-right: 0;
+  }
+  .row--wide img {
+    margin-bottom: 6px;
+  }
+  .col-50 {
+    width: 100%;
+    margin-bottom: 12px;
+  }
+  .row {
+    padding: 0 18px;
+  }
+  .docs .highlight,
+  .codepen-embed {
+    padding-left: 18px;
+    padding-right: 18px;
+    margin-left: -18px;
+  }
+  .section {
+    margin-top: 88px;
+  }
+  .docs {
+    margin-top: 32px;
+  }
+  .section--head {
+    margin-top:0;
+    padding: 24px 0;
+  }
+  .row--nav {
+    font-size: 18px;
+    line-height: 26px; 
+  }
+
+
+}
+
+@media screen and (max-width: 700px) {
+
+  .row--nav {
+    font-size: 18px;
+    line-height: 26px; 
+  }
+
+  .demo-gallery {
+    max-width: 500px;
+  }
+  .demo-gallery a {
+    width: 120px;
+    margin: 0 4px 4px 0;
+  }
+  a.demo-gallery__img--main {
+    width: 164px;
+  }
+  .section--head p {
+    font-size: 18px;
+    line-height: 24px;
+  }
+}
+
+/*@media screen and (max-width: 490px) {
+  .demo-gallery a {
+    width: 100px;
+    margin: 0 4px 4px 0;
+  }
+  a.demo-gallery__img--main {
+    width: 137px;
+  }
+}
+*/
+
+
+@media screen and (max-width: 450px) {
+  .demo-gallery a {
+    width: 95px;
+    margin: 0 1px 1px 0;
+  }
+  a.demo-gallery__img--main {
+    width: 127px;
+  }
+}
+
+
+@media screen and (max-width: 350px) {
+  .demo-gallery a {
+    width: 81px;
+margin: 0 1px 1px 0;
+  }
+  a.demo-gallery__img--main {
+    width: 109px;
+  }
+}
+
+
+
+
+

+ 71 - 0
assets/site.js

@@ -0,0 +1,71 @@
+  function xdr(url, method, data, callback, errback) {
+    var req;
+    if (!callback)
+      callback = function(){};
+    if (!errback)
+      errback = function(){};
+
+    if(XMLHttpRequest && ('withCredentials' in (req = new XMLHttpRequest()))) {
+      req.open(method, url, true);
+      req.withCredentials = "true";
+      req.onerror = errback;
+      req.onreadystatechange = function() {
+        if (req.readyState === 4) {
+          if (req.status >= 200 && req.status < 400) {
+            callback(req.responseText);
+          } else {
+            errback(new Error('Response returned with non-OK status'), req);
+          }
+        }
+      };
+      req.setRequestHeader("Accept", "application/json");
+      if (data)
+        req.setRequestHeader("Content-type", "application/json");
+      req.send(data);
+    } else if(XDomainRequest) {
+      var reqUrl = url;
+      if (url.indexOf('http:') === 0) {
+        reqUrl = url.substr(5);
+      }
+      else if (url.indexOf('https:') === 0) {
+        reqUrl = url.substr(6);
+      }
+      req = new XDomainRequest();
+      req.open(method, reqUrl);
+      req.onerror = function(err) { errback(err, req); };
+      req.onload = function() {
+        callback(req.responseText);
+      };
+      req.ontimeout = function() {};
+      req.onprogress = function() {};
+      req.timeout = 0;
+      req.send(data);
+    } else {
+      errback(new Error('CORS not supported'));
+    }
+  };
+  function jsonGET(url, callback, errback){
+    xdr(url, 'GET', null, function(data){
+      callback(JSON.parse(data));
+    }, errback);
+  }
+  function jsonPOST(url, data, callback, errback){
+    xdr(url, 'POST', data, function(data){
+      callback(JSON.parse(data));
+    }, errback);
+  }
+
+function startGallery(items, index) {
+
+    var pswpElement = document.querySelectorAll('.pswp')[0];
+
+    var options = {
+        index: index || 0
+    };
+
+    // Initializes and opens PhotoSwipe
+    var gallery = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options);
+    gallery.init();
+
+    return gallery;
+}

+ 47 - 0
exif.lisp

@@ -0,0 +1,47 @@
+(in-package #:photo-store)
+
+(defun degrees-to-rational (deg ref)
+  (when deg
+    (let ((rational
+           (+ (elt deg 0)
+              (/ (elt deg 1) 60)
+              (/ (elt deg 2) 3600))))
+      (if (member ref '("N" "E") :test #'equal)
+          rational
+          (- rational)))))
+
+(defun exif-to-point (exif)
+  (when exif
+    (let ((lat (degrees-to-rational (zpb-exif:exif-value :GPSLatitude exif)
+                                    (zpb-exif:exif-value :GPSLatitudeRef exif)))
+          (lon (degrees-to-rational (zpb-exif:exif-value :GPSLongitude exif)
+                                    (zpb-exif:exif-value :GPSLongitudeRef exif))))
+      (and lat lon (geo:point-deg lat lon)))))
+
+(defun exif-to-taken (exif)
+  (when exif
+    (or (zpb-exif:parsed-exif-value :DateTimeOriginal exif)
+        (zpb-exif:parsed-exif-value :DateTime exif))))
+
+(defun exif-to-dim (exif)
+  (when exif
+    (let ((width (and exif (or (zpb-exif:exif-value :PixelXDimension exif)
+                               (zpb-exif:exif-value :ImageWidth exif))))
+          (height (and exif (or (zpb-exif:exif-value :PixelYDimension exif)
+                                (zpb-exif:exif-value :ImageHeight exif)))))
+      (and width height (list width height)))))
+
+(defun load-photo-info (path)
+  (with-open-file (in path :element-type '(unsigned-byte 8))
+    (let ((length (file-length in))
+          (modified (file-write-date path))
+          (exif (ignore-errors (zpb-exif:make-exif in))))
+      (list
+       (cons :name (pathname-name path))
+       (cons :modified modified)
+       (cons :length length)
+       (cons :created-at (local-time:universal-to-timestamp
+                          (or (exif-to-taken exif) modified)))
+       (cons :dim (exif-to-dim exif))
+       (cons :location (exif-to-point exif))))))
+

+ 88 - 0
index.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html lang="en" prefix="og: http://ogp.me/ns#" debug="true">
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">    
+    <meta charset="utf-8">
+
+    <title>Photo store</title>
+
+    <meta name="viewport" content="width = device-width, initial-scale = 1.0"> 
+
+    <link rel="stylesheet" href="/css/site.css">
+    <script src="/js/site.js"></script>
+
+    <!-- Core CSS file -->
+    <link rel="stylesheet" href="/css/photoswipe.css"> 
+
+    <!-- Skin CSS file (styling of UI - buttons, caption, etc.)
+         In the folder of skin CSS file there are also:
+         - .png and .svg icons sprite, 
+         - preloader.gif (for browsers that do not support CSS animations) -->
+    <link rel="stylesheet" href="/css/default-skin.css"> 
+
+    <!-- Core JS file -->
+    <script src="/js/photoswipe.min.js"></script> 
+
+    <!-- UI JS file -->
+    <script src="/js/photoswipe-ui-default.min.js"></script> 
+
+    <!--[if lt IE 9]>
+        <script>
+          document.createElement('figure');
+        </script>
+    <![endif]-->
+  </head>
+
+  <body>
+	<div class="section section--head">
+	  <div class="row row--heading">
+		<h1>PhotoStore</h1>
+	  </div>
+    </div>
+
+    <div id="gallery" class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
+      <div class="pswp__bg"></div>
+      <div class="pswp__scroll-wrap">
+        <div class="pswp__container">
+		  <div class="pswp__item"></div>
+		  <div class="pswp__item"></div>
+		  <div class="pswp__item"></div>
+        </div>
+        <div class="pswp__ui pswp__ui--hidden">
+          <div class="pswp__top-bar">
+			<div class="pswp__counter"></div>
+			<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
+			<button class="pswp__button pswp__button--share" title="Share"></button>
+			<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
+			<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
+			<div class="pswp__preloader">
+			  <div class="pswp__preloader__icn">
+				<div class="pswp__preloader__cut">
+				  <div class="pswp__preloader__donut"></div>
+				</div>
+			  </div>
+			</div>
+          </div>
+		  <!-- <div class="pswp__loading-indicator"><div class="pswp__loading-indicator__line"></div></div> -->
+          <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
+	        <div class="pswp__share-tooltip">
+	        </div>
+	      </div>
+
+          <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>
+          <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>
+          <div class="pswp__caption">
+            <div class="pswp__caption__center">
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <script type="text/javascript">
+      jsonGET('/incoming/', function(data) {
+          startGallery(data);
+      });
+    </script>
+  </body>
+</html>

+ 0 - 2
package.lisp

@@ -4,5 +4,3 @@
   (:nicknames #:ph-store)
   (:use :cl)
   (:export #:start))
-
-(in-package #:photo-store)

+ 3 - 0
photo-store.asd

@@ -7,8 +7,11 @@
                :geo
                :local-time
                :log4cl
+               :restas
                :zpb-exif
                )
   :serial t
   :components ((:file "package")
+               (:file "utils")
+               (:file "exif")
                (:file "photo-store")))

+ 63 - 1
photo-store.lisp

@@ -1,3 +1,65 @@
 (in-package #:photo-store)
 
-(defun start ())
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (restas::register-pkgmodule-traits 'photo-store)
+  (restas:reconnect-all-routes))
+
+
+;; photos: id | album_id | filename | size | taken | width | height | lat | lon
+;; albums: id | parent_id | path | date | name | description | cover_id | accessibility
+
+(defstruct photo url title width height)
+
+
+(defun load-photo (path)
+  (let ((info (load-photo-info path)))
+    (make-photo :url (map-path path)
+                :title (aget :name info)
+                :width (first (aget :dim info))
+                :height (second (aget :dim info)))))
+
+(defun load-photos-from-dir (directory)
+  (mapcar 'load-photo (remove-if-not #'(lambda (p)
+                                         (equal (string-downcase (pathname-type p)) "jpg"))
+                                     (uiop:directory-files directory))))
+
+(defvar *path-url-mapping* nil "alist of path-2-url mapping")
+(defun map-path (path)
+  (loop for (base-path . base-url) in *path-url-mapping*
+     for rel-path = (uiop:subpathp path base-path)
+     when rel-path do (return (concatenate 'string base-url (namestring rel-path)))
+     finally (error "Can't map path ~S" path)))
+
+(defmethod yason:encode ((photo photo) &optional (stream *standard-output*))
+  (yason:with-output (stream)
+    (yason:with-object ()
+      (yason:encode-object-element "src" (photo-url photo))
+      (yason:encode-object-element "w" (photo-width photo))
+      (yason:encode-object-element "h" (photo-height photo))
+      (yason:encode-object-element "title" (photo-title photo)))))
+
+(restas:define-route main ("")
+  (asdf:system-relative-pathname :photo-store "index.html"))
+
+;; Assets file path
+(defparameter *assets-path*
+  (asdf:system-relative-pathname :photo-store "assets/"))
+
+(restas:define-route assets/css ("css/:file" :content-type "text/css")  
+  (merge-pathnames file *assets-path*))
+
+(restas:define-route assets/js ("js/:file" :content-type "application/x-javascript")
+  (merge-pathnames file *assets-path*))
+
+
+(defvar *incoming-path* nil "Path for incoming photos")
+(restas:define-route incoming ("incoming/"
+                               :content-type "application/json")
+  (with-output-to-string (stream)
+    (yason:encode
+     (loop for subdir in (uiop:subdirectories *incoming-path*)
+        append (load-photos-from-dir subdir))
+     stream)))
+
+(defun start (&key (address "0.0.0.0") (port 8800))
+  (restas:start '#:photo-store :address address :port port))

+ 110 - 0
utils.lisp

@@ -0,0 +1,110 @@
+(in-package #:photo-store)
+
+
+(defun aget (key alist)
+  (cdr (assoc key alist :test #'equal)))
+
+
+;; From 'Practical Common Lisp' by Peter Seibel
+(defun component-present-p (value)
+  (and value (not (eql value :unspecific))))
+
+(defun directory-pathname-p (p)
+  (and
+   (not (component-present-p (pathname-name p)))
+   (not (component-present-p (pathname-type p)))
+   p))
+
+(defun pathname-as-directory (name)
+  (let ((pathname (pathname name)))
+    (when (wild-pathname-p pathname)
+      (error "Can't reliably convert wild pathnames."))
+    (if (not (directory-pathname-p name))
+      (make-pathname
+       :directory (append (or (pathname-directory pathname) (list :relative))
+                          (list (file-namestring pathname)))
+       :name      nil
+       :type      nil
+       :defaults pathname)
+      pathname)))
+
+(defun directory-wildcard (dirname)
+  (make-pathname
+   :name :wild
+   :type #-clisp :wild #+clisp nil
+   :defaults (pathname-as-directory dirname)))
+
+(defun list-directory (dirname)
+  (when (wild-pathname-p dirname)
+    (error "Can only list concrete directory names."))
+  (let ((wildcard (directory-wildcard dirname)))
+
+    #+(or sbcl cmu lispworks)
+    (directory wildcard)
+
+    #+openmcl
+    (directory wildcard :directories t)
+
+    #+allegro
+    (directory wildcard :directories-are-files nil)
+
+    #+clisp
+    (nconc
+     (directory wildcard)
+     (directory (clisp-subdirectories-wildcard wildcard)))
+
+    #-(or sbcl cmu lispworks openmcl allegro clisp)
+    (error "list-directory not implemented")))
+
+#+clisp
+(defun clisp-subdirectories-wildcard (wildcard)
+  (make-pathname
+   :directory (append (pathname-directory wildcard) (list :wild))
+   :name nil
+   :type nil
+   :defaults wildcard))
+
+(defun file-exists-p (pathname)
+  #+(or sbcl lispworks openmcl)
+  (probe-file pathname)
+
+  #+(or allegro cmu)
+  (or (probe-file (pathname-as-directory pathname))
+      (probe-file pathname))
+
+  #+clisp
+  (or (ignore-errors
+        (probe-file (pathname-as-file pathname)))
+      (ignore-errors
+        (let ((directory-form (pathname-as-directory pathname)))
+          (when (ext:probe-directory directory-form)
+            directory-form))))
+
+  #-(or sbcl cmu lispworks openmcl allegro clisp)
+  (error "file-exists-p not implemented"))
+
+(defun pathname-as-file (name)
+  (let ((pathname (pathname name)))
+    (when (wild-pathname-p pathname)
+      (error "Can't reliably convert wild pathnames."))
+    (if (directory-pathname-p name)
+      (let* ((directory (pathname-directory pathname))
+             (name-and-type (pathname (first (last directory)))))
+        (make-pathname
+         :directory (butlast directory)
+         :name (pathname-name name-and-type)
+         :type (pathname-type name-and-type)
+         :defaults pathname))
+      pathname)))
+
+(defun walk-directory (dirname fn &key directories (test (constantly t)))
+  (labels
+      ((walk (name)
+         (cond
+           ((directory-pathname-p name)
+            (when (and directories (funcall test name))
+              (funcall fn name))
+            (when (funcall test name)
+              (dolist (x (list-directory name)) (walk x))))
+           ((funcall test name) (funcall fn name)))))
+    (walk (pathname-as-directory dirname))))

Some files were not shown because too many files changed in this diff