pax_global_header00006660000000000000000000000064121530106100014477gustar00rootroot0000000000000052 comment=3e6551cdc7c1d6a06bac5e49039fdc0189911b0b photofloat-0~20120917+dfsg/000077500000000000000000000000001215301061000153225ustar00rootroot00000000000000photofloat-0~20120917+dfsg/scanner/000077500000000000000000000000001215301061000167535ustar00rootroot00000000000000photofloat-0~20120917+dfsg/scanner/.gitignore000066400000000000000000000000371215301061000207430ustar00rootroot00000000000000upload.sh *.pyc cache/* test/* photofloat-0~20120917+dfsg/scanner/CachePath.py000066400000000000000000000026371215301061000211550ustar00rootroot00000000000000import os.path from datetime import datetime def message(category, text): if message.level <= 0: sep = " " else: sep = "--" print "%s %s%s[%s]%s%s" % (datetime.now().isoformat(), max(0, message.level) * " |", sep, category, max(1, (14 - len(category))) * " ", text) message.level = -1 def next_level(): message.level += 1 def back_level(): message.level -= 1 def set_cache_path_base(base): trim_base.base = base def untrim_base(path): return os.path.join(trim_base.base, path) def trim_base_custom(path, base): if path.startswith(base): path = path[len(base):] if path.startswith('/'): path = path[1:] return path def trim_base(path): return trim_base_custom(path, trim_base.base) def cache_base(path): path = trim_base(path).replace('/', '-').replace(' ', '_').replace('(', '').replace('&', '').replace(',', '').replace(')', '').replace('#', '').replace('[', '').replace(']', '').replace('"', '').replace("'", '').replace('_-_', '-').lower() while path.find("--") != -1: path = path.replace("--", "-") while path.find("__") != -1: path = path.replace("__", "_") if len(path) == 0: path = "root" return path def json_cache(path): return cache_base(path) + ".json" def image_cache(path, size, square=False): if square: suffix = str(size) + "s" else: suffix = str(size) return cache_base(path) + "_" + suffix + ".jpg" def file_mtime(path): return datetime.fromtimestamp(int(os.path.getmtime(path))) photofloat-0~20120917+dfsg/scanner/PhotoAlbum.py000066400000000000000000000323061215301061000214030ustar00rootroot00000000000000from CachePath import * from datetime import datetime import json import os import os.path from PIL import Image from PIL.ExifTags import TAGS import gc class Album(object): def __init__(self, path): self._path = trim_base(path) self._photos = list() self._albums = list() self._photos_sorted = True self._albums_sorted = True @property def photos(self): return self._photos @property def albums(self): return self._albums @property def path(self): return self._path def __str__(self): return self.path @property def cache_path(self): return json_cache(self.path) @property def date(self): self._sort() if len(self._photos) == 0 and len(self._albums) == 0: return datetime(1900, 1, 1) elif len(self._photos) == 0: return self._albums[-1].date elif len(self._albums) == 0: return self._photos[-1].date return max(self._photos[-1].date, self._albums[-1].date) def __cmp__(self, other): return cmp(self.date, other.date) def add_photo(self, photo): self._photos.append(photo) self._photos_sorted = False def add_album(self, album): self._albums.append(album) self._albums_sorted = False def _sort(self): if not self._photos_sorted: self._photos.sort() self._photos_sorted = True if not self._albums_sorted: self._albums.sort() self._albums_sorted = True @property def empty(self): if len(self._photos) != 0: return False if len(self._albums) == 0: return True for album in self._albums: if not album.empty: return False return True def cache(self, base_dir): self._sort() fp = open(os.path.join(base_dir, self.cache_path), 'w') json.dump(self, fp, cls=PhotoAlbumEncoder) fp.close() @staticmethod def from_cache(path): fp = open(path, "r") dictionary = json.load(fp) fp.close() return Album.from_dict(dictionary) @staticmethod def from_dict(dictionary, cripple=True): album = Album(dictionary["path"]) for photo in dictionary["photos"]: album.add_photo(Photo.from_dict(photo, untrim_base(album.path))) if not cripple: for subalbum in dictionary["albums"]: album.add_album(Album.from_dict(subalbum), cripple) album._sort() return album def to_dict(self, cripple=True): self._sort() subalbums = [] if cripple: for sub in self._albums: if not sub.empty: subalbums.append({ "path": trim_base_custom(sub.path, self._path), "date": sub.date }) else: for sub in self._albums: if not sub.empty: subalbums.append(sub) return { "path": self.path, "date": self.date, "albums": subalbums, "photos": self._photos } def photo_from_path(self, path): for photo in self._photos: if trim_base(path) == photo._path: return photo return None class Photo(object): thumb_sizes = [ (75, True), (150, True), (640, False), (800, False), (1024, False) ] def __init__(self, path, thumb_path=None, attributes=None): self._path = trim_base(path) self.is_valid = True try: mtime = file_mtime(path) except KeyboardInterrupt: raise except: self.is_valid = False return if attributes is not None and attributes["dateTimeFile"] >= mtime: self._attributes = attributes return self._attributes = {} self._attributes["dateTimeFile"] = mtime try: image = Image.open(path) except KeyboardInterrupt: raise except: self.is_valid = False return self._metadata(image) self._thumbnails(image, thumb_path, path) def _metadata(self, image): self._attributes["size"] = image.size self._orientation = 1 try: info = image._getexif() except KeyboardInterrupt: raise except: return if not info: return exif = {} for tag, value in info.items(): decoded = TAGS.get(tag, tag) if isinstance(value, str): value = value.strip().partition("\x00")[0] if isinstance(decoded, str) and decoded.startswith("DateTime"): try: value = datetime.strptime(value, '%Y:%m:%d %H:%M:%S') except KeyboardInterrupt: raise except: continue exif[decoded] = value if "Orientation" in exif: self._orientation = exif["Orientation"]; if self._orientation in range(5, 9): self._attributes["size"] = (self._attributes["size"][1], self._attributes["size"][0]) if self._orientation - 1 < len(self._metadata.orientation_list): self._attributes["orientation"] = self._metadata.orientation_list[self._orientation - 1] if "Make" in exif: self._attributes["make"] = exif["Make"] if "Model" in exif: self._attributes["model"] = exif["Model"] if "ApertureValue" in exif: self._attributes["aperture"] = exif["ApertureValue"] elif "FNumber" in exif: self._attributes["aperture"] = exif["FNumber"] if "FocalLength" in exif: self._attributes["focalLength"] = exif["FocalLength"] if "ISOSpeedRatings" in exif: self._attributes["iso"] = exif["ISOSpeedRatings"] if "ISO" in exif: self._attributes["iso"] = exif["ISO"] if "PhotographicSensitivity" in exif: self._attributes["iso"] = exif["PhotographicSensitivity"] if "ExposureTime" in exif: self._attributes["exposureTime"] = exif["ExposureTime"] if "Flash" in exif and exif["Flash"] in self._metadata.flash_dictionary: try: self._attributes["flash"] = self._metadata.flash_dictionary[exif["Flash"]] except KeyboardInterrupt: raise except: pass if "LightSource" in exif and exif["LightSource"] in self._metadata.light_source_dictionary: try: self._attributes["lightSource"] = self._metadata.light_source_dictionary[exif["LightSource"]] except KeyboardInterrupt: raise except: pass if "ExposureProgram" in exif and exif["ExposureProgram"] < len(self._metadata.exposure_list): self._attributes["exposureProgram"] = self._metadata.exposure_list[exif["ExposureProgram"]] if "SpectralSensitivity" in exif: self._attributes["spectralSensitivity"] = exif["SpectralSensitivity"] if "MeteringMode" in exif and exif["MeteringMode"] < len(self._metadata.metering_list): self._attributes["meteringMode"] = self._metadata.metering_list[exif["MeteringMode"]] if "SensingMethod" in exif and exif["SensingMethod"] < len(self._metadata.sensing_method_list): self._attributes["sensingMethod"] = self._metadata.sensing_method_list[exif["SensingMethod"]] if "SceneCaptureType" in exif and exif["SceneCaptureType"] < len(self._metadata.scene_capture_type_list): self._attributes["sceneCaptureType"] = self._metadata.scene_capture_type_list[exif["SceneCaptureType"]] if "SubjectDistanceRange" in exif and exif["SubjectDistanceRange"] < len(self._metadata.subject_distance_range_list): self._attributes["subjectDistanceRange"] = self._metadata.subject_distance_range_list[exif["SubjectDistanceRange"]] if "ExposureCompensation" in exif: self._attributes["exposureCompensation"] = exif["ExposureCompensation"] if "ExposureBiasValue" in exif: self._attributes["exposureCompensation"] = exif["ExposureBiasValue"] if "DateTimeOriginal" in exif: self._attributes["dateTimeOriginal"] = exif["DateTimeOriginal"] if "DateTime" in exif: self._attributes["dateTime"] = exif["DateTime"] _metadata.flash_dictionary = {0x0: "No Flash", 0x1: "Fired",0x5: "Fired, Return not detected",0x7: "Fired, Return detected",0x8: "On, Did not fire",0x9: "On, Fired",0xd: "On, Return not detected",0xf: "On, Return detected",0x10: "Off, Did not fire",0x14: "Off, Did not fire, Return not detected",0x18: "Auto, Did not fire",0x19: "Auto, Fired",0x1d: "Auto, Fired, Return not detected",0x1f: "Auto, Fired, Return detected",0x20: "No flash function",0x30: "Off, No flash function",0x41: "Fired, Red-eye reduction",0x45: "Fired, Red-eye reduction, Return not detected",0x47: "Fired, Red-eye reduction, Return detected",0x49: "On, Red-eye reduction",0x4d: "On, Red-eye reduction, Return not detected",0x4f: "On, Red-eye reduction, Return detected",0x50: "Off, Red-eye reduction",0x58: "Auto, Did not fire, Red-eye reduction",0x59: "Auto, Fired, Red-eye reduction",0x5d: "Auto, Fired, Red-eye reduction, Return not detected",0x5f: "Auto, Fired, Red-eye reduction, Return detected"} _metadata.light_source_dictionary = {0: "Unknown", 1: "Daylight", 2: "Fluorescent", 3: "Tungsten (incandescent light)", 4: "Flash", 9: "Fine weather", 10: "Cloudy weather", 11: "Shade", 12: "Daylight fluorescent (D 5700 - 7100K)", 13: "Day white fluorescent (N 4600 - 5400K)", 14: "Cool white fluorescent (W 3900 - 4500K)", 15: "White fluorescent (WW 3200 - 3700K)", 17: "Standard light A", 18: "Standard light B", 19: "Standard light C", 20: "D55", 21: "D65", 22: "D75", 23: "D50", 24: "ISO studio tungsten"} _metadata.metering_list = ["Unknown", "Average", "Center-weighted average", "Spot", "Multi-spot", "Multi-segment", "Partial"] _metadata.exposure_list = ["Not Defined", "Manual", "Program AE", "Aperture-priority AE", "Shutter speed priority AE", "Creative (Slow speed)", "Action (High speed)", "Portrait", "Landscape", "Bulb"] _metadata.orientation_list = ["Horizontal (normal)", "Mirror horizontal", "Rotate 180", "Mirror vertical", "Mirror horizontal and rotate 270 CW", "Rotate 90 CW", "Mirror horizontal and rotate 90 CW", "Rotate 270 CW"] _metadata.sensing_method_list = ["Not defined", "One-chip color area sensor", "Two-chip color area sensor", "Three-chip color area sensor", "Color sequential area sensor", "Trilinear sensor", "Color sequential linear sensor"] _metadata.scene_capture_type_list = ["Standard", "Landscape", "Portrait", "Night scene"] _metadata.subject_distance_range_list = ["Unknown", "Macro", "Close view", "Distant view"] def _thumbnail(self, image, thumb_path, original_path, size, square=False): thumb_path = os.path.join(thumb_path, image_cache(self._path, size, square)) info_string = "%s -> %spx" % (os.path.basename(original_path), str(size)) if square: info_string += ", square" message("thumbing", info_string) if os.path.exists(thumb_path) and file_mtime(thumb_path) >= self._attributes["dateTimeFile"]: return gc.collect() try: image = image.copy() except KeyboardInterrupt: raise except: try: image = image.copy() # we try again to work around PIL bug except KeyboardInterrupt: raise except: message("corrupt image", os.path.basename(original_path)) return if square: if image.size[0] > image.size[1]: left = (image.size[0] - image.size[1]) / 2 top = 0 right = image.size[0] - ((image.size[0] - image.size[1]) / 2) bottom = image.size[1] else: left = 0 top = (image.size[1] - image.size[0]) / 2 right = image.size[0] bottom = image.size[1] - ((image.size[1] - image.size[0]) / 2) image = image.crop((left, top, right, bottom)) gc.collect() image.thumbnail((size, size), Image.ANTIALIAS) try: image.save(thumb_path, "JPEG", quality=88) except KeyboardInterrupt: os.unlink(thumb_path) raise except: message("save failure", os.path.basename(thumb_path)) os.unlink(thumb_path) def _thumbnails(self, image, thumb_path, original_path): mirror = image if self._orientation == 2: # Vertical Mirror mirror = image.transpose(Image.FLIP_LEFT_RIGHT) elif self._orientation == 3: # Rotation 180 mirror = image.transpose(Image.ROTATE_180) elif self._orientation == 4: # Horizontal Mirror mirror = image.transpose(Image.FLIP_TOP_BOTTOM) elif self._orientation == 5: # Horizontal Mirror + Rotation 270 mirror = image.transpose(Image.FLIP_TOP_BOTTOM).transpose(Image.ROTATE_270) elif self._orientation == 6: # Rotation 270 mirror = image.transpose(Image.ROTATE_270) elif self._orientation == 7: # Vertical Mirror + Rotation 270 mirror = image.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270) elif self._orientation == 8: # Rotation 90 mirror = image.transpose(Image.ROTATE_90) for size in Photo.thumb_sizes: self._thumbnail(mirror, thumb_path, original_path, size[0], size[1]) @property def name(self): return os.path.basename(self._path) def __str__(self): return self.name @property def path(self): return self._path @property def image_caches(self): return [image_cache(self._path, size[0], size[1]) for size in Photo.thumb_sizes] @property def date(self): if not self.is_valid: return datetime(1900, 1, 1) if "dateTimeOriginal" in self._attributes: return self._attributes["dateTimeOriginal"] elif "dateTime" in self._attributes: return self._attributes["dateTime"] else: return self._attributes["dateTimeFile"] def __cmp__(self, other): date_compare = cmp(self.date, other.date) if date_compare == 0: return cmp(self.name, other.name) return date_compare @property def attributes(self): return self._attributes @staticmethod def from_dict(dictionary, basepath): del dictionary["date"] path = os.path.join(basepath, dictionary["name"]) del dictionary["name"] for key, value in dictionary.items(): if key.startswith("dateTime"): try: dictionary[key] = datetime.strptime(dictionary[key], "%a %b %d %H:%M:%S %Y") except KeyboardInterrupt: raise except: pass return Photo(path, None, dictionary) def to_dict(self): photo = { "name": self.name, "date": self.date } photo.update(self.attributes) return photo class PhotoAlbumEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.strftime("%a %b %d %H:%M:%S %Y") if isinstance(obj, Album) or isinstance(obj, Photo): return obj.to_dict() return json.JSONEncoder.default(self, obj) photofloat-0~20120917+dfsg/scanner/TreeWalker.py000066400000000000000000000070531215301061000213770ustar00rootroot00000000000000import os import os.path import sys from datetime import datetime from PhotoAlbum import Photo, Album, PhotoAlbumEncoder from CachePath import * import json class TreeWalker: def __init__(self, album_path, cache_path): self.album_path = os.path.abspath(album_path).decode(sys.getfilesystemencoding()) self.cache_path = os.path.abspath(cache_path).decode(sys.getfilesystemencoding()) set_cache_path_base(self.album_path) self.all_albums = list() self.all_photos = list() self.walk(self.album_path) self.big_lists() self.remove_stale() message("complete", "") def walk(self, path): next_level() message("walking", os.path.basename(path)) cache = os.path.join(self.cache_path, json_cache(path)) cached = False cached_album = None if os.path.exists(cache): try: cached_album = Album.from_cache(cache) if file_mtime(path) <= file_mtime(cache): message("full cache", os.path.basename(path)) cached = True album = cached_album for photo in album.photos: self.all_photos.append(photo) else: message("partial cache", os.path.basename(path)) except KeyboardInterrupt: raise except: message("corrupt cache", os.path.basename(path)) cached_album = None if not cached: album = Album(path) for entry in os.listdir(path): if entry[0] == '.': continue try: entry = entry.decode(sys.getfilesystemencoding()) except KeyboardInterrupt: raise except: pass entry = os.path.join(path, entry) if os.path.isdir(entry): album.add_album(self.walk(entry)) elif not cached and os.path.isfile(entry): next_level() cache_hit = False if cached_album: cached_photo = cached_album.photo_from_path(entry) if cached_photo and file_mtime(entry) <= cached_photo.attributes["dateTimeFile"]: message("cache hit", os.path.basename(entry)) cache_hit = True photo = cached_photo if not cache_hit: message("metainfo", os.path.basename(entry)) photo = Photo(entry, self.cache_path) if photo.is_valid: self.all_photos.append(photo) album.add_photo(photo) else: message("unreadable", os.path.basename(entry)) back_level() if not album.empty: message("caching", os.path.basename(path)) album.cache(self.cache_path) self.all_albums.append(album) else: message("empty", os.path.basename(path)) back_level() return album def big_lists(self): photo_list = [] self.all_photos.sort() for photo in self.all_photos: photo_list.append(photo.path) message("caching", "all photos path list") fp = open(os.path.join(self.cache_path, "all_photos.json"), 'w') json.dump(photo_list, fp, cls=PhotoAlbumEncoder) fp.close() photo_list.reverse() message("caching", "latest photos path list") fp = open(os.path.join(self.cache_path, "latest_photos.json"), 'w') json.dump(photo_list[0:27], fp, cls=PhotoAlbumEncoder) fp.close() def remove_stale(self): message("cleanup", "building stale list") all_cache_entries = { "all_photos.json": True, "latest_photos.json": True } for album in self.all_albums: all_cache_entries[album.cache_path] = True for photo in self.all_photos: for entry in photo.image_caches: all_cache_entries[entry] = True message("cleanup", "searching for stale cache entries") for cache in os.listdir(self.cache_path): try: cache = cache.decode(sys.getfilesystemencoding()) except KeyboardInterrupt: raise except: pass if cache not in all_cache_entries: message("cleanup", os.path.basename(cache)) os.unlink(os.path.join(self.cache_path, cache)) photofloat-0~20120917+dfsg/scanner/main.py000077500000000000000000000006471215301061000202630ustar00rootroot00000000000000#!/usr/bin/env python from TreeWalker import TreeWalker from CachePath import message import sys def main(): reload(sys) sys.setdefaultencoding("UTF-8") if len(sys.argv) != 3: print "usage: %s ALBUM_PATH CACHE_PATH" % sys.argv[0] return try: TreeWalker(sys.argv[1], sys.argv[2]) except KeyboardInterrupt: message("keyboard", "CTRL+C pressed, quitting.") sys.exit(-97) if __name__ == "__main__": main() photofloat-0~20120917+dfsg/web/000077500000000000000000000000001215301061000160775ustar00rootroot00000000000000photofloat-0~20120917+dfsg/web/.gitignore000066400000000000000000000000121215301061000200600ustar00rootroot00000000000000upload.sh photofloat-0~20120917+dfsg/web/.htaccess000066400000000000000000000014751215301061000177040ustar00rootroot00000000000000AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/javascript application/json Header set Cache-Control "max-age=29030400, public" Header set Cache-Control "max-age=5184000, public" Header set Cache-Control "max-age=2678400, public" Header set Cache-Control "max-age=3600, public" deny from all RewriteEngine On RewriteBase / RewriteRule ^redirect\.php$ - [L] RewriteCond %{QUERY_STRING} _escaped_fragment_= RewriteRule . staticrender.php [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /redirect.php [L] photofloat-0~20120917+dfsg/web/Makefile000066400000000000000000000020621215301061000175370ustar00rootroot00000000000000JS_DIR = js CSS_DIR = css JS_MIN = $(JS_DIR)/scripts.min.js CSS_MIN = $(CSS_DIR)/styles.min.css JS_MIN_FILES := $(sort $(patsubst %.js, %.min.js, $(filter-out %.min.js, $(wildcard $(JS_DIR)/*.js)))) CSS_MIN_FILES := $(sort $(patsubst %.css, %.min.css, $(filter-out %.min.css, $(wildcard $(CSS_DIR)/*.css)))) JS_COMPILER = yui-compressor --type js CSS_COMPILER = yui-compressor --type css .PHONY: all clean all: $(JS_MIN) $(CSS_MIN) %.min.js: %.js @echo "Compiling javascript" $< @$(JS_COMPILER) -o $@ $< %.min.css: %.css @echo "Compiling stylesheet" $< @$(CSS_COMPILER) -o $@ $< $(JS_MIN): $(JS_MIN_FILES) @echo "Assembling compiled javascripts" @cat $^ > $@ $(CSS_MIN): $(CSS_MIN_FILES) @echo "Assembling compiled stylesheets" @cat $^ > $@ empty := space := $(empty) $(empty) classpath := $(subst $(space),:,$(wildcard /usr/share/java/htmlunit*.jar)) utils/ServerExecute.class: utils/ServerExecute.java @echo "Building HtmlUnit wrapper." @javac -classpath $(classpath) -d utils $^ clean: @rm -fv $(JS_MIN) $(JS_MIN_FILES) $(CSS_MIN) $(CSS_MIN_FILES) photofloat-0~20120917+dfsg/web/css/000077500000000000000000000000001215301061000166675ustar00rootroot00000000000000photofloat-0~20120917+dfsg/web/css/.gitignore000066400000000000000000000000121215301061000206500ustar00rootroot00000000000000*.min.css photofloat-0~20120917+dfsg/web/css/.htaccess000066400000000000000000000001011215301061000204550ustar00rootroot00000000000000 deny from all photofloat-0~20120917+dfsg/web/css/000-controls.css000066400000000000000000000070341215301061000215450ustar00rootroot00000000000000body { margin: 0; padding: 0; background-color: #222222; font-family: "LM Roman", "Georgia", "Palatino Linotype", "Palatino", "Times New Roman", "Times", serif; color: #FFFFFF; } a { color: #84AAC2; text-decoration: none; } a:hover { color: #FFAD27; } #title { position: absolute; top: 0; padding: 0.4em; font-weight: bold; font-size: 1.15em; } #loading { display: none; } #album-view { position: absolute; top: 2.5em; padding: 1em; } #thumbs { clear: both; line-height: 0; } #thumbs img { border: 0; margin: 0; padding: 0; } .current-thumb { border-top: 1px solid #FFAD27 !important; } #subalbums { padding-top: 1.5em; } .album-button { float: left; display: block; width: 150px; height: 60px; text-align: center; font-style: italic; font-size: 12px; background-repeat: no-repeat; background-position: top; padding-top: 150px; background-image: url(../img/image-placeholder.png); } #next, #back { position: absolute; width: auto; font-size: 4.5em; line-height: 0; top: 40%; font-weight: bold; opacity: 0.35; -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=35)"; filter: alpha(opacity=35); } #back { left: 0.1em; } #next { right: 0.1em; } #photo { border: 0; left: 0; } #photo-view { position: absolute; bottom: 150px; top: 2.5em; overflow: hidden; margin-bottom: 0.5em; left: 0; right: 0; text-align: center; } #photo-box { display: inline; } #photo-links { background-color: #000000; font-weight: bold; height: 10px; font-size: 10px; line-height: 7px; padding-top: 3px; padding-bottom: 3px; padding-right: 10px; padding-left: 10px; display: none; border-top-right-radius: 5px; border-top-left-radius: 5px; -moz-border-top-right-radius: 5px; -moz-border-top-left-radius: 5px; -webkit-border-top-right-radius: 5px; -webkit-border-top-left-radius: 5px; } #metadata { background-color: #000000; width: 340px; font-size: 12px; line-height: 12px; padding-top: 3px; padding-bottom: 3px; padding-right: 10px; padding-left: 10px; display: none; margin: 0 auto; margin-top: 1px; border-top-right-radius: 5px; border-top-left-radius: 5px; -moz-border-top-right-radius: 5px; -moz-border-top-left-radius: 5px; -webkit-border-top-right-radius: 5px; -webkit-border-top-left-radius: 5px; opacity: 0.5; -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; filter: alpha(opacity=50); overflow: auto; } #metadata table { margin: auto auto; text-align: left; } #metadata tr { height: 14px; } #photo-bar { position: absolute; bottom: 0; width: 100%; text-align: center; } #fullscreen, #fullscreen-divider { display: none; } .photo-view-container { position: absolute; height: 150px; width: 100%; bottom: 0; top: auto !important; overflow-x: auto; overflow-y: hidden; white-space: nowrap; padding: 0 !important; text-align: center; } #powered-by { clear: both; text-align: center; font-size: 0.85em; font-style: italic; font-weight: bold; text-shadow: rgba(0,0,0,0.5) -1px 0, rgba(0,0,0,0.3) 0 -1px, rgba(255,255,255,0.5) 0 1px, rgba(0,0,0,0.3) -1px -2px; color: #FFAD27; } #error-overlay, #error-text, #auth-text { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none; } #error-overlay { background-color: #000000; } #error-text, #auth-text { position: fixed; padding-top: 20%; text-align: center; } #error-text { font-size: 4em; font-weight: bold; font-style: italic; } #auth-text input { color: rgb(0, 0, 0); background-color: rgb(200, 200, 200); border: 0; font-family: inherit; font-size: 2em; font-weight: bold; font-style: italic; } photofloat-0~20120917+dfsg/web/css/001-fonts.css000066400000000000000000000011561215301061000210330ustar00rootroot00000000000000@font-face { font-family: "LM Roman"; font-weight: bold; font-style: normal; src: url("../fonts/lmroman10-bold.otf"); } @font-face { font-family: "LM Roman"; font-weight: bold; font-style: italic; src: url("../fonts/lmroman10-bolditalic.otf"); } @font-face { font-family: "LM Roman"; font-weight: normal; font-style: italic; src: url("../fonts/lmroman10-italic.otf"); } @font-face { font-family: "LM Roman"; font-weight: normal; font-style: normal; src: url("../fonts/lmroman10-regular.otf"); }photofloat-0~20120917+dfsg/web/css/002-mobile.css000066400000000000000000000003111215301061000211420ustar00rootroot00000000000000@media handheld, only screen and (max-height: 640px) { #photo-view { top: 0; bottom: 0; margin: 0; } .photo-view-container { display: none; } #title { font-size: 1em; z-index: 1; } } photofloat-0~20120917+dfsg/web/img/000077500000000000000000000000001215301061000166535ustar00rootroot00000000000000photofloat-0~20120917+dfsg/web/img/image-placeholder.png000066400000000000000000000661661215301061000227420ustar00rootroot00000000000000PNG  IHDR<qsRGBbKGD pHYs7]7]F] IDATxyeU;by*UsT%UIhB Bpj6^j7^۫`1A 4*լ*+_x}s{"J jγVxs~ps\7us\7us\7us\7us\7us\7us\7us\7us\[brߛz%Ѓ%Yd_7pPlnai Qig xL)sӞnW hrN 01cr9f T` ;-}9ۻu_?=oR&f9ଵTb(B@nt"5, aH ˘}NJ{ݿ25jgoͰ1ݗqe8-e!lw {H2$-_;&qNDklyv`Xi÷ű& ;P ۶oi[fװܺ[1\B?(Ƞhrm3fMw3v-UdqSHqqpg$_gҞ]ŏm-+k 2M(ڊ5xMX7&C+[!0@Ah &G@lLQ&q Ka!0ڠWcD˻,`Iysyr~p+,:[2J9l[F )`!]E6o+zXu 4@P37G%eE')0C=B{3ac >_z}~ӿؤO#$clFcBMs4oyz7r\rB0Do%7'ABė $Ɣ&3ĝXǬ\jP=p2$~,ľ;rUz<BQF545 L2xA B HqTH)R$3hMP\dk(l"X k\!=)MPR~Ùu,?/-/ ]!+`FmF׈?f ogl"33 6_<_},zʌ1{0wu<X#V]?pOQq :N=ssFc==f 4Vװ3h7>=Y 1R^:*2\È%Q |bT8z`+AXOB`BHB"@Wg3qb,K,H3 o/Zd6n+9g0YkD2k}o42nxaIZMWНwVdHhhU/ѨШv_~gQta_ ᚹn(F\]l\Ű茄P`21I(IF mZKbT*"$jKq|cw&"c,K`;xc6N~2>,luVLW_Kh2AR1RJ# 1Fl3O`o䙅:q F7c!qo5Wzۯ`:AbP'Xe4ICM,!%,?+[&:vx a+X&,_Kjc<>`}[{0=IlQ#L~L0QW `c!QtQbl#Aؗht44t *50)d/&8+2⚷Anh#Xo+^ZjgƊ1Kf[N)Zs c]azZ3^gEAj[,M3=x! 0| 5~^3`<M R8Gd(@:/=vB&MȣW5^Q2{w N9HّϽ~Vh4812hb/ig-"[%ľ@]DBH|Ė@t`[VA$-O7ڑh;8dTC#N|eo) 9t$Nr'C<߁aI0HmiZu[J%2HV" êh/)%1E Eg>ϖDee?MW [jAHl0N% I6o k-dCk|]J("*R\dzRtXo7B<-ȍM$0.li0!=vlpB>ry,u J7CĨd# ܹy;j 8X)P[]\@L#5/vp.V^ P"HaAD"eokRBꡐ:hHbl\gag}.XH["B 8ԤOòшg![HʡJB_lԧ`2݇~.! >+H<߶l>XGDZ=LvIlK^/\g(yzy*ec,u1s,I,5m %J$q5G-V"bǦLhoBJͺ%@!ɅnkG9P4>r\3_ |j +bYAz>wP07z!OS)H*z@>Pâ[F{ uRU)4+!mЌ|ukvM !bldn Ӱ$5v[vw _u'br׎XHn{;QybYalb$Nb|#S&5/䜱X -B\FرݱW7ɏɆKI=W筮 ok)^v[ףvbB!:(4nӎ߲7~u|_}䃔Oz%we^~Ez,F*ϼBq7P\ݡT69v/dq#+;r4a)UŒF$%K"mV<~f>UB蓗p2qղP#!k`ET1tↇyU RȞ~3"1"$|+(c$sv*X݋&|gk M_\e v0ABu ^ K5qy/Eٙ`1;v|'߼JC(@ 5V^&oѪ]c_#d@$@Ϻ!x֐R63pn|N1bR!Ұ`CթmTVz2-&3ݣHox*ڦ]n%%"o#J22Y͘鰼$6}BP[z%Rd CΨptGcuAx/ )3=r=R(H,'gh˄hAV+E(F}{ >捿̉?⹇?Nni̡q׳P+%㐟ESvvM%Rtlk5Uy,BXB0Uw{ ث[ 4JN #m,UVs$S d[AjfIUpCzwg‹YRPqmˡQ0y~En띘\Op1V^zovcr/ dzEe @z8ksrY-UEP:@LP!Q!6elt i1 NE+:A !Alk2&'c%FUrf<_1WpjOaoU\`,v8A ޻\},pRH!ڣʴ\7aJa\H.6 Bhmrem^M\x,)8o305 a4Nd3Pod#w%s :[ 抰TKM?^ƋWT [8rm\; `czKj: >NDނ &c<\\ QY{ 6}6Ԫr8J뾢{#ZjS[o2>iS6 GFg;v!R`R-0 @:u+ef ‹/Jl7K|u>NvEgY,* -A9dGWDm=LWB#gP>=T^Mq+B+=|#{%<6o/[{VZ9tVamqjɣvu^wO}ݤ9n.hRqK;j0U<;5|vi /_"jYZX[Ddd%"Hh7`-QM2BQDhaXyFHˡ.&} Q1}{2i4KD Msf/*z눮-z$w'{ȣazTE?4R^Le\}O4>]mKzχA "8/E^yɄg"Q1BݔeNyVe6:<~c4ctFկo}kurV >X4E[d&ZɠC[b -"VeS`mՑ%9ܯS2Jlp+bSѢEOg.b`5'sim VIt1u ]#*wZ$RI}ldeX %05'9A^EMG~W$0 -"?&C'\܃AuCÒ:oqҸY{ j $)I2bDZxdtɊ |b1k &lϤZxG)-2m(#u1E6/ H'3`xGEDsaXURֲ癶R`Gey]/om#Paz!CJbCc׮ <ɣG1Lca:-;ؼp-h8^p'a+"_JW3bS&{ s6#n5mDĪMǮQ* E) ! ()):.SQ mK:7\ 83zs,4UJnAh/oQK7 by'e3;")CqB|\sߏZ PF$i7"fPEnvVYN: q*M{,Z様?Z\=FdN>{5igZ^#9x % HcuBy M(xJ4ecd*yڗZ; IDAT;\;=,LӑncFĈw+;A7&ղ^=d*3}!҆ jGL#XYRP,\RYIfY+#<~^drfRgq|5z-=u6-L2HDQO$է.˨2"Ӥ iW nfӊ q ,B+ځF%d2d}_zH\շ+5+l :5GDrP^_kD N ⚝J,U}\sQdhuGr ;6nFB2{Ϩi89/98f-N#E2N,6m>ju&]P7Q31?Ey}֠wwH7׾%# P9`i/ϲ8ZZ/BfE$;h]!]@\ f&w|;Ct<0n˵n] DB~=rKH8  `_ާ`#j}n4h$%[DJ6[T!G+8#WLgbr2*Wm;c5"(JRVvI(N&6"2\ Cd#4lWĞ<)1OG,/fS ƨYbew;6tzGttrrLJE:Jbl'?H;-:(Ï?˟~}DRc|'۩MlÁ݈bP@ޒM6UDa 1lKB:{ AcJzҭxѝB2^~ y,ou hP_^_,Nm,1}`)fls0›]۷G`ݰ7@l+-?t2I+^a\[Y 蘨Xl eQ:pӷEnz?~~ bAVtBz3JӟgHrzQ)I\(u$$Ð;:ٷ~L ?ڨC5w['X_C( ,O`N:t F&7k{טPGR>ek dBgE,nP%oi`BZxA]EΜezz-/2Q۷8 t:Mx4o$zYBn`p԰Ρ!0C-{V}"N+)gg(Nr;F$]zPTf ϦS}UW($sNL?,^Xq7\;!yZ.Scy~yUV/>Qo9]V <֤"f@t?6jdz3flVkDn!YxKc~tv7xv\֦٨iAr9=("1qڵ&zMC!;1DJt'm:nq*N=FixJs;qRq[wwX. BN1g"eN8'B5)PJk2sƤClzli/p`DLѱ8>(015IVY1:<Ϗc\7(M Msf]Nr5.<0j*1Iv7: odL3 R[8_4mͫ͸@ye#43Nyڍ:QsfG>W`[?qȶE7Tyv^Sp"5mImbKC!됛=I`4Bƺ Ǡ{Bf0NO[0E+ټ~ڛ:63{poW|>?,_ 1Z*Bۧrx_!u/ |N%#ˌ[bd)hXî-R-"Z݉®X qRVL,+8pJǬ-05=_ M2QRhW)g(_V<~:M'Ilʋ$KV^sk 321=ŶlfJ6[%Mц|o}U5b;Zz5̰ERǣÖ~F~ic`a3 u1bt8v,b1xe{sLu06džXC'J2~cg:D)g2<\9{[v9 jOޯg?o?t/6$}v?rAC.O?8 au*N7aN]{I~[ǀG$Ñ>yjdťA w;zbא+Tdj<:/ykW,|3iW0Vy:F6ms}[awoZ- Pki>zsj0Qb#Q@F!jUXHPȺ]JBG*K֦ގ̌5`@᱌adVA$t6Z~lQk4+u9\j8 xoe/R^^Ѯw0Ӝ 5<&Op۔KlA[Kߦ}m whFX]V]R٦aN!@{T}y>NAnq%vn=xڟeޓd #4{dm^©'ؾ;rؖ_/'"\ -7Ԫu*W02ũݼߋ/-(f9y/-dqL~sϽWnX: W!Ϩ>;(Zf;7PxYSc)H5P#1C:BWKiX[f&*}Ʊ]L/7ƹ/FD|"-|fhAgO'~c<[_ǎsכBI)Ɗ>cE1[g_^is?-Gwưm(N!Tnu6hmTu98ӸqLqlSvXYC$s`| zؒL/=u "evo=gX=rQQqxәbۿeV"AizaN+S{8]QL{%tŮ f#*܂=}~ޠz;a7/pǏ߅ǘj vp!!##NMdJ1Jp-C:Q;nm 6?Jva-e~uBt, Sdb߭*pWTVp,>r{{+co"bI!vD?.w Ԍ :8sı&n+&fҬ1F[2 ^\)biuȵy_b&T^4H$$fsԷ+1N<s_ziLOu+w38na"<&c+ӯN(jڕ26 i0ZVD>__b\eem;am;Sd"v çr|铟Gtr3+e8|iv(9*Yߥndice sǐ~cJ-}KSt,|OrKU喅:谅+j 6Ж9rZ1k/Cx >؎C)пcB4;O=:4; tSjbssvdYYv?f9kqY22-_bz}m=c^:iYI!)URHlwYOIn c{NQkF;S1H0-LVe z,F#r%[lG/"~kO~K/B|2z'N?- !n41jYP@Uq'ys*Gr.<,-2+ILBuX }(&r,"K*6B|)%zjE\+x_`lLM2%ҒXa]otarS2v|),'帽|ۆ$hnJ4BĽi4B;M/cEsu7>Fy!`ϫ&l5({_*3;1ͳ%nG@?yO`;|;c4鬞2{LXĶ<+}x/k K\Z'o]Y{`.\vCx鐹:ÿO@ Ac0 6:y]Ǫ,^hT ?rޛIzwy߼3뾫[ݭHS0Y쵰=c10^OxM̬w5 f"Fa|` -u>|y7FQ]]]UY={pǨc')S,^WP]Yg% '$/.BT@t,lE)2^LeksC(mE:'!ұ_,tPx)f/ Ruy {Ns蔤/g1|M87K5u,<3@3uOE\[CEoަBjCTbdYʟRΡ~Qu%t<3Fɥow7!N"Eh{u8J 2%*Aw@79{ dW%f殰!NNv"M%]9(S7dyv ZR4'16I:#Hf,y=eYiMuT a"&|bh4*[abZ(fLBc5v|=Bg!wqFh 0B )vƦ0ϝ'Mq~>4V }Z0hUgsl>03VRJW+B:TH \k:d nNnП?P!MJMʗX@l\e*Oͱ}V#+089c=O较]|{&/&z| F6}gLq2mi 7E,dt5iҊbѽ}BB!16L.s{261J *rI&#-CMwEkt z?w<1P5=}IV^gy}r2{a.?k#f:Cn>h& O~Cb`ɍVKtQƢܴM{+{o *P!HvfK3u'YkL?5L ƒL]&3>~僌?ʥHlhPsS* OD核5JN\L; 66v)ĭ0 Ii-fM08 g DגưP|kXGP74|CҨgxr r.8 $A$ IDATEYR;hff |J+AX(צ4)hm?zk,* 3dr<(U #Hk:2!#VP&c@UxeFFÐ~2yɵkS%5hkw^}'8qĪ))&v1X&Y8f L+˕eTN쇹D!=@C/H6ZCӨj80^ >l<K)^ yavuROyT죾A%,wOT>4Lul3Bרt M`3zWy2La {L3\<`2Qn!K&%:c*t=$^ϞJ'S ߷(`a]閰599yl >R) `esY\ WMF]$ո8i*5ag3))s[?~ƥAKH8d Y^~j=i:BR,vduy a \*)TĬ$[_E..N*s5VKM-n:[4:]Wn^ ՘5?ŷnKnsy Ϟa8v&G([iǍ?JU`sq ==()@iF+ҚzNlBB]ߕ4mh_-#8Bv{&)nZPNHAOf;IMXvǬìV亲q-I)V_'eفѺ bg>*M&f~niP(bR5J\A*aP#TZx0PI ;0|Kנ"0:uCkk-XEbª@ ҡm9,-mNkCѶn."W"U`xt7,%67ж [KaM=z:u >V.SnsxL\X{C ~pύpml?IFWxh0]K`)!cd|Q2 i.,5ky D:KOH <|z) LXiW#"@;l&R(;1mݴsj4UQGW9gOxn!f{>y7Bj4mԛufr!>wtz1⦦tu<4b/ݬb bden'GbrBCs)rS_,˔yC k0fylpllM×vb&+eT*Ûri2,ՍUNPgbjFxx)f8|K?QJBK͆ӏfB`Px[v1VM; N6l \UI^]u aAKb;%I4|?Ywy,}VEdxGF<4:>u*fĨh`5h?w+{ka| WY_*C>C8O4ylŏ6Zz %0798ú60(DHNh/LL{RFux UeތFd_EU}agnxgFFGcGz@Y:e_ptv{ڰVVy Ljvd6zK[֢$e-Er.!}33|ϟѵGn8I2L D:Kh6Fb;&vkBZX'ɲ lF&j8 {z!gyBTsпRtxs4Kn1ӵ}Mc!8[k}tǧ}&zS֬//Dz̳EPDJ^FWqkO7J=%q/a΅5z:k Z&b}ԂmĉrH )P{ 1:iMGNvq7ihfgnR2Y.4V|o}TO/^zzEk0)0=TMN(YIТ:ʏmFj;& #E?|q5%Z+Hqqil nba6WVp}X_h 1vF |wjj\~e!jv$=/š.uUStڅDv86^6ScU|WOPǏ+dƶ2آUuNYsg~(Q2l%Iy̮RuVՍoTcMfG$$|4w :~55iڠBu&swӿgwU9M?O~vAu݁55Uc;{Z&|&lB6sAUZ}49> Ƀ'w݃9nɍ(3s3"{ԟ瀅  ,Y2xFK͡b 7!2!FsUL:LFva9UbIyf7L_q#j^ՕE՞!ʆH 7[&] KU̞g6m2_HgUDOsRZ6Es9h)\ljPHPa/0 K B`ۂB6`o,\\6M_)p/ýTnJfu<3f|Oӟ0j+/]ؙ^v[k7M$ЏPSY[a 7R/{1N5Մm+ܷE" Ƴsk\]p4<1N:% ˦ )#YkxuMd҂F#NCoXVý>7BgtQ#1ص:\b}gF|yq]QݏO ̅AB'cBvTYZH˲bXk9 %uZ{|c@Ý磖-Y! ٔM 5 1?Ii0J] :j*tGL!#f(BY%[XyCyLsz3feeʅ ') e !+DlGvOdd0a4(V-`\G[޹`n0Ek2jܼ$&O4dLq`'e)X^^w _RGu8Ћ-^X9Θ̆/ [3*cJ1X/ sWUN߸^*MJ,KZ:AZ*M=L!#V[&ZWf0JTBM \&ß|cÏ~f 47^T`u ouq@>ђR0u !{/Hݦ> Q*rptӒz2l0?"# 9<7DϞ݈T<a&4 cŊTam'FdcyaK+f38 }fQ"c4ViDԚG 5+^~B10>E+ߑGG?q)ֺbEcE/:ѕ պM+Fl9'k܆,$\;,TgsYe\ff2KX؇!^ ;dr9 Cl6[}H~N)4` Ӭ "zTd׸ zġ=r8MFP7>!qFẰZ7 T#Rs+aP{/5#wzH20;]&dp.H"uT21Bo7\Tpz2~2,Ҷ87pH7ɥ#Q}v.[,<ՍeD* 좱p l.O4!-hZ|2P0i;ͮ{P#9š)J\-X,Tٕ/rg|aspRCd6B^t @z>8(J<߼}})0|sݢ !6^H_$ :>Wtc m[_[dwWv ]]|RY#K*%^_mµˬi<*?A=/}ogQT?0:"Mq +X u@:05Tj,iƧ`4>vZYP ?|- 5a4qW&@x^@iTjK<<ץ8Ʋm=sw,ֺuVY7q:[\wWā |x*/%ZTZ6{igBD <5_g @>;~*Uz vqv!b `PKr-IPݨR{afjeDZec:|XIWQwdygh4d{X]FHg SYoftvH|V&z:s3~uTp2|ҳ~X6b Hۦѓ-"TEwR* 6vvXLWSŷ%^Da#.brÔvg8k:\XabeLYi,=% G@Vg.yn}iJYlfe.4:>֗\E:%ũ}EJ%zU'@AN6!˅_qLʲs{ʤӗ׭KЦe.FH(' 6nwGhO?A$7Vx Y`i,%c"af/\` .,Q@`)egYee"')FpW^cW C(odY] {)3T2%G l6@ĩSƲ3JDw[Di >1d׈=BFqm1.Oi lE>۹JZ虄2N|PL-lW IDAT{Ia(.d 3h`xt y{Yx,8#ӤsE,+MK`;b5< Xel6Zi%zR*j!|w;\;T'd߸%?GZ`/gLPJ-bdh>`KPD.έAoWivb+R5%p&J͂Hѻ"̳O14Wp;IillVg0PE(xbSl,$?4O[2B)qɓd2c~Au]a粹bDX I"NtiaF޴7Ū}Is.0ID7<߼M)eɮr[~R*CCȨ`E:&cA7g|&)RYzm,MX*b4o;0PMoŽv7]r,gl&5X:CK8jmD\7=P[ l)$)HlL dhYb UVc@(7B[$`T#Ϯ-X+o0%JyƆzM?L7S|w^rKa%GnB1GRũc6 )puj (OTѣd2?z2l!=la6w%vp=y.1E\3r!{^1@X2ށ[,kdvMԡgίpm=xQ2B^zB0_6pAya:g1ٟf_P r96+z3E؇Ҵ,O"`IM*O@ >X!A8Ev'çkFWgeL鐴v[a"i ^h0,}J6UF=s<1۠dYk B,?*]Vh\#9Wɠ"lZb:J)1==}x]snkufּ@3kE˕XMtsL_FFrhU 6*&؇C֤"b'L كp7-3uDǐ2@I~P^`:Dd|ϙť'XB#E hZ0KЯ&|AmVou'-.^;_d2/s,Ӯ!Kko|Ѵ0]]Xu(&?FivTnP55HmKfasŘ#OrkTXҖƒV1:VJā1q!ow;\pg!^0 )um=J \0]mp pZRm]V2&G +P``*RGvpQ ,~w(Dtđ e@U P`H(bpLi!PK6=@wc Wϼۿ__mۗ$ꇿYHхF˒"rt SMLjьRw*Ɖ;.cxNF)ki,D+ FX.rԈZXjV:VH+Ui4yMگ.0~)+ʍM IlMx& غHT$EXU8+Ѳ#@c-6k2%/ci ~C`ǐ,[~ W&dHV&bBV|P@#xk~oᱻ~3ƘAmÜiWX>V $uhf'S=_yg<zC_>c?A*T}V63_"mA\ Ci57?:ldLMTCw5Z A )>g}FP.)F*6ÂC=ȴ3VpGlnnjm칧{?e nǹB1&  1"bSy^31I7AD-;G|$?:v윝sv9;g윝sv9;g윝sv9;g윝sv9;g윝sv9;s?)̅T(-IENDB`photofloat-0~20120917+dfsg/web/index.html000066400000000000000000000025761215301061000201060ustar00rootroot00000000000000 PhotoFloat
Photos
Loading...
Powered by PhotoFloat
Forgot my camera.
photofloat-0~20120917+dfsg/web/js/000077500000000000000000000000001215301061000165135ustar00rootroot00000000000000photofloat-0~20120917+dfsg/web/js/.gitignore000066400000000000000000000000111215301061000204730ustar00rootroot00000000000000*.min.js photofloat-0~20120917+dfsg/web/js/.htaccess000066400000000000000000000000711215301061000203070ustar00rootroot00000000000000 deny from all photofloat-0~20120917+dfsg/web/js/001-hashchange.js000066400000000000000000000400531215301061000214420ustar00rootroot00000000000000/*! * jQuery hashchange event - v1.3 - 7/21/2010 * http://benalman.com/projects/jquery-hashchange-plugin/ * * Copyright (c) 2010 "Cowboy" Ben Alman * Dual licensed under the MIT and GPL licenses. * http://benalman.com/about/license/ */ // Script: jQuery hashchange event // // *Version: 1.3, Last updated: 7/21/2010* // // Project Home - http://benalman.com/projects/jquery-hashchange-plugin/ // GitHub - http://github.com/cowboy/jquery-hashchange/ // Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js // (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped) // // About: License // // Copyright (c) 2010 "Cowboy" Ben Alman, // Dual licensed under the MIT and GPL licenses. // http://benalman.com/about/license/ // // About: Examples // // These working examples, complete with fully commented code, illustrate a few // ways in which this plugin can be used. // // hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/ // document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/ // // About: Support and Testing // // Information about what version or versions of jQuery this plugin has been // tested with, what browsers it has been tested in, and where the unit tests // reside (so you can test it yourself). // // jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2 // Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5, // Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5. // Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/ // // About: Known issues // // While this jQuery hashchange event implementation is quite stable and // robust, there are a few unfortunate browser bugs surrounding expected // hashchange event-based behaviors, independent of any JavaScript // window.onhashchange abstraction. See the following examples for more // information: // // Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/ // Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/ // WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/ // Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/ // // Also note that should a browser natively support the window.onhashchange // event, but not report that it does, the fallback polling loop will be used. // // About: Release History // // 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more // "removable" for mobile-only development. Added IE6/7 document.title // support. Attempted to make Iframe as hidden as possible by using // techniques from http://www.paciellogroup.com/blog/?p=604. Added // support for the "shortcut" format $(window).hashchange( fn ) and // $(window).hashchange() like jQuery provides for built-in events. // Renamed jQuery.hashchangeDelay to and // lowered its default value to 50. Added // and properties plus document-domain.html // file to address access denied issues when setting document.domain in // IE6/7. // 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin // from a page on another domain would cause an error in Safari 4. Also, // IE6/7 Iframe is now inserted after the body (this actually works), // which prevents the page from scrolling when the event is first bound. // Event can also now be bound before DOM ready, but it won't be usable // before then in IE6/7. // 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug // where browser version is incorrectly reported as 8.0, despite // inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag. // 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special // window.onhashchange functionality into a separate plugin for users // who want just the basic event & back button support, without all the // extra awesomeness that BBQ provides. This plugin will be included as // part of jQuery BBQ, but also be available separately. (function($,window,undefined){ '$:nomunge'; // Used by YUI compressor. // Reused string. var str_hashchange = 'hashchange', // Method / object references. doc = document, fake_onhashchange, special = $.event.special, // Does the browser support window.onhashchange? Note that IE8 running in // IE7 compatibility mode reports true for 'onhashchange' in window, even // though the event isn't supported, so also test document.documentMode. doc_mode = doc.documentMode, supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 ); // Get location.hash (or what you'd expect location.hash to be) sans any // leading #. Thanks for making this necessary, Firefox! function get_fragment( url ) { url = url || location.href; return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' ); }; // Method: jQuery.fn.hashchange // // Bind a handler to the window.onhashchange event or trigger all bound // window.onhashchange event handlers. This behavior is consistent with // jQuery's built-in event handlers. // // Usage: // // > jQuery(window).hashchange( [ handler ] ); // // Arguments: // // handler - (Function) Optional handler to be bound to the hashchange // event. This is a "shortcut" for the more verbose form: // jQuery(window).bind( 'hashchange', handler ). If handler is omitted, // all bound window.onhashchange event handlers will be triggered. This // is a shortcut for the more verbose // jQuery(window).trigger( 'hashchange' ). These forms are described in // the section. // // Returns: // // (jQuery) The initial jQuery collection of elements. // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and // $(elem).hashchange() for triggering, like jQuery does for built-in events. $.fn[ str_hashchange ] = function( fn ) { return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange ); }; // Property: jQuery.fn.hashchange.delay // // The numeric interval (in milliseconds) at which the // polling loop executes. Defaults to 50. // Property: jQuery.fn.hashchange.domain // // If you're setting document.domain in your JavaScript, and you want hash // history to work in IE6/7, not only must this property be set, but you must // also set document.domain BEFORE jQuery is loaded into the page. This // property is only applicable if you are supporting IE6/7 (or IE8 operating // in "IE7 compatibility" mode). // // In addition, the property must be set to the // path of the included "document-domain.html" file, which can be renamed or // modified if necessary (note that the document.domain specified must be the // same in both your main JavaScript as well as in this file). // // Usage: // // jQuery.fn.hashchange.domain = document.domain; // Property: jQuery.fn.hashchange.src // // If, for some reason, you need to specify an Iframe src file (for example, // when setting document.domain as in ), you can // do so using this property. Note that when using this property, history // won't be recorded in IE6/7 until the Iframe src file loads. This property // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7 // compatibility" mode). // // Usage: // // jQuery.fn.hashchange.src = 'path/to/file.html'; $.fn[ str_hashchange ].delay = 50; /* $.fn[ str_hashchange ].domain = null; $.fn[ str_hashchange ].src = null; */ // Event: hashchange event // // Fired when location.hash changes. In browsers that support it, the native // HTML5 window.onhashchange event is used, otherwise a polling loop is // initialized, running every milliseconds to // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7 // compatibility" mode), a hidden Iframe is created to allow the back button // and hash-based history to work. // // Usage as described in : // // > // Bind an event handler. // > jQuery(window).hashchange( function(e) { // > var hash = location.hash; // > ... // > }); // > // > // Manually trigger the event handler. // > jQuery(window).hashchange(); // // A more verbose usage that allows for event namespacing: // // > // Bind an event handler. // > jQuery(window).bind( 'hashchange', function(e) { // > var hash = location.hash; // > ... // > }); // > // > // Manually trigger the event handler. // > jQuery(window).trigger( 'hashchange' ); // // Additional Notes: // // * The polling loop and Iframe are not created until at least one handler // is actually bound to the 'hashchange' event. // * If you need the bound handler(s) to execute immediately, in cases where // a location.hash exists on page load, via bookmark or page refresh for // example, use jQuery(window).hashchange() or the more verbose // jQuery(window).trigger( 'hashchange' ). // * The event can be bound before DOM ready, but since it won't be usable // before then in IE6/7 (due to the necessary Iframe), recommended usage is // to bind it inside a DOM ready handler. // Override existing $.event.special.hashchange methods (allowing this plugin // to be defined after jQuery BBQ in BBQ's source code). special[ str_hashchange ] = $.extend( special[ str_hashchange ], { // Called only when the first 'hashchange' event is bound to window. setup: function() { // If window.onhashchange is supported natively, there's nothing to do.. if ( supports_onhashchange ) { return false; } // Otherwise, we need to create our own. And we don't want to call this // until the user binds to the event, just in case they never do, since it // will create a polling loop and possibly even a hidden Iframe. $( fake_onhashchange.start ); }, // Called only when the last 'hashchange' event is unbound from window. teardown: function() { // If window.onhashchange is supported natively, there's nothing to do.. if ( supports_onhashchange ) { return false; } // Otherwise, we need to stop ours (if possible). $( fake_onhashchange.stop ); } }); // fake_onhashchange does all the work of triggering the window.onhashchange // event for browsers that don't natively support it, including creating a // polling loop to watch for hash changes and in IE 6/7 creating a hidden // Iframe to enable back and forward. fake_onhashchange = (function(){ var self = {}, timeout_id, // Remember the initial hash so it doesn't get triggered immediately. last_hash = get_fragment(), fn_retval = function(val){ return val; }, history_set = fn_retval, history_get = fn_retval; // Start the polling loop. self.start = function() { timeout_id || poll(); }; // Stop the polling loop. self.stop = function() { timeout_id && clearTimeout( timeout_id ); timeout_id = undefined; }; // This polling loop checks every $.fn.hashchange.delay milliseconds to see // if location.hash has changed, and triggers the 'hashchange' event on // window when necessary. function poll() { var hash = get_fragment(), history_hash = history_get( last_hash ); if ( hash !== last_hash ) { history_set( last_hash = hash, history_hash ); $(window).trigger( str_hashchange ); } else if ( history_hash !== last_hash ) { location.href = location.href.replace( /#.*/, '' ) + history_hash; } timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay ); }; // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv $.browser.msie && !supports_onhashchange && (function(){ // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8 // when running in "IE7 compatibility" mode. var iframe, iframe_src; // When the event is bound and polling starts in IE 6/7, create a hidden // Iframe for history handling. self.start = function(){ if ( !iframe ) { iframe_src = $.fn[ str_hashchange ].src; iframe_src = iframe_src && iframe_src + get_fragment(); // Create hidden Iframe. Attempt to make Iframe as hidden as possible // by using techniques from http://www.paciellogroup.com/blog/?p=604. iframe = $('