Add photo albums and hook them up to the blog

This commit is contained in:
Noah 2014-04-05 00:47:43 -07:00
parent 19034f5b69
commit 91536e3d05
43 changed files with 7133 additions and 7 deletions

View File

@ -25,6 +25,9 @@ SECRET_KEY = 'for the love of Arceus, change this key!'
# Password strength: number of iterations for bcrypt to hash passwords. # Password strength: number of iterations for bcrypt to hash passwords.
BCRYPT_ITERATIONS = 12 BCRYPT_ITERATIONS = 12
# Where to save temp files for photo uploads, etc.
TEMPDIR = "/tmp"
# Rophako uses a flat file JSON database system, and the Redis caching server # Rophako uses a flat file JSON database system, and the Redis caching server
# sits between Ropahko and the filesystem. # sits between Ropahko and the filesystem.
DB_ROOT = "db" DB_ROOT = "db"
@ -33,10 +36,31 @@ REDIS_PORT = 6379
REDIS_DB = 0 REDIS_DB = 0
REDIS_PREFIX = "rophako:" REDIS_PREFIX = "rophako:"
# Blog settings ################################################################################
## Blog Settings ##
################################################################################
BLOG_ENTRIES_PER_PAGE = 5 # Number of entries to show per page BLOG_ENTRIES_PER_PAGE = 5 # Number of entries to show per page
BLOG_ENTRIES_PER_RSS = 5 # The same, but for the RSS feed BLOG_ENTRIES_PER_RSS = 5 # The same, but for the RSS feed
BLOG_DEFAULT_CATEGORY = "Uncategorized" BLOG_DEFAULT_CATEGORY = "Uncategorized"
BLOG_DEFAULT_PRIVACY = "public" BLOG_DEFAULT_PRIVACY = "public"
BLOG_TIME_FORMAT = "%A, %B %d %Y @ %I:%M:%S %p" # "Weekday, Month dd yyyy @ hh:mm:ss AM" BLOG_TIME_FORMAT = "%A, %B %d %Y @ %I:%M:%S %p" # "Weekday, Month dd yyyy @ hh:mm:ss AM"
BLOG_ALLOW_COMMENTS = True BLOG_ALLOW_COMMENTS = True
################################################################################
## Photo Settings ##
################################################################################
# The path to where uploaded photos will be stored.
# The PRIVATE path is from the perspective of the server file system.
# The PUBLIC path is from the perspective of the web browser via HTTP.
PHOTO_ROOT_PRIVATE = os.path.join(_basedir, "site", "www", "static", "photos")
PHOTO_ROOT_PUBLIC = "/static/photos"
PHOTO_DEFAULT_ALBUM = "My Photos" # Default/fallback album name.
PHOTO_TIME_FORMAT = BLOG_TIME_FORMAT
# Photo sizes.
PHOTO_WIDTH_LARGE = 800 # Max width of full size photos.
PHOTO_WIDTH_THUMB = 256 # Max square width of photo thumbnails.
PHOTO_WIDTH_AVATAR = 96 # Square width of photo avatars.

View File

@ -1,3 +1,5 @@
flask flask
redis redis
bcrypt bcrypt
pillow
requests

View File

@ -18,9 +18,11 @@ app.secret_key = config.SECRET_KEY
from rophako.modules.admin import mod as AdminModule from rophako.modules.admin import mod as AdminModule
from rophako.modules.account import mod as AccountModule from rophako.modules.account import mod as AccountModule
from rophako.modules.blog import mod as BlogModule from rophako.modules.blog import mod as BlogModule
from rophako.modules.photo import mod as PhotoModule
app.register_blueprint(AdminModule) app.register_blueprint(AdminModule)
app.register_blueprint(AccountModule) app.register_blueprint(AccountModule)
app.register_blueprint(BlogModule) app.register_blueprint(BlogModule)
app.register_blueprint(PhotoModule)
# Custom Jinja handler to support custom- and default-template folders for # Custom Jinja handler to support custom- and default-template folders for
# rendering templates. # rendering templates.
@ -50,6 +52,7 @@ def before_request():
"name": "Rophako", "name": "Rophako",
"version": __version__, "version": __version__,
"author": "Noah Petherbridge", "author": "Noah Petherbridge",
"photo_url": config.PHOTO_ROOT_PUBLIC,
}, },
"uri": request.path.split("/")[1:], "uri": request.path.split("/")[1:],
"session": { "session": {

542
rophako/model/photo.py Normal file
View File

@ -0,0 +1,542 @@
# -*- coding: utf-8 -*-
"""Photo album models."""
import os
from flask import g, request
import time
import requests
from PIL import Image
import hashlib
import random
import config
import rophako.jsondb as JsonDB
from rophako.utils import sanitize_name
from rophako.log import logger
# Maps the friendly names of photo sizes with their pixel values from config.
PHOTO_SCALES = dict(
large=config.PHOTO_WIDTH_LARGE,
thumb=config.PHOTO_WIDTH_THUMB,
avatar=config.PHOTO_WIDTH_AVATAR,
)
def list_albums():
"""Retrieve a sorted list of the photo albums."""
index = get_index()
result = []
for album in index["album-order"]:
cover = index["covers"][album]
pic = index["albums"][album][cover]["thumb"]
result.append(dict(
name=album,
cover=pic,
data=index["albums"][album],
))
return result
def list_photos(album):
"""List the photos in an album."""
album = sanitize_name(album)
index = get_index()
if not album in index["albums"]:
return None
result = []
for key in index["photo-order"][album]:
data = index["albums"][album][key]
result.append(dict(
key=key,
data=data,
))
return result
def photo_exists(key):
"""Query whether a photo exists in the album."""
index = get_index()
return key in index["map"]
def get_photo(key):
"""Look up a photo by key. Returns None if not found."""
index = get_index()
photo = None
if key in index["map"]:
album = index["map"][key]
if album in index["albums"] and key in index["albums"][album]:
photo = index["albums"][album][key]
else:
# The map is wrong!
logger.error("Photo album map is wrong; key {} not found in album {}!".format(key, album))
del index["map"][key]
write_index(index)
if not photo:
return None
# Inject additional information about the photo:
# What position it's at in the album, how many photos total, and the photo
# IDs of its siblings.
siblings = index["photo-order"][album]
i = 0
print siblings
for pid in siblings:
if pid == key:
# We found us! Find the siblings.
sprev, snext = None, None
if i == 0:
# We're the first photo. So previous is the last!
sprev = siblings[-1]
if len(siblings) > i+1:
snext = siblings[i+1]
else:
snext = key
elif i == len(siblings)-1:
# We're the last. So next is first!
sprev = siblings[i-1]
snext = siblings[0]
else:
# Right in the middle.
print "MIDDLE!"
sprev = siblings[i-1]
snext = siblings[i+1]
# Inject.
photo["album"] = album
photo["position"] = i+1
photo["siblings"] = len(siblings)
photo["previous"] = sprev
photo["next"] = snext
i += 1
return photo
def get_image_dimensions(pic):
"""Use PIL to get the image's true dimensions."""
filename = os.path.join(config.PHOTO_ROOT_PRIVATE, pic["large"])
img = Image.open(filename)
return img.size
def update_photo(album, key, data):
"""Update photo meta-data in the album."""
index = get_index()
if not album in index["albums"]:
index["albums"][album] = {}
if not key in index["albums"][album]:
index["albums"][album][key] = {}
# Update!
index["albums"][album][key].update(data)
write_index(index)
def crop_photo(key, x, y, length):
"""Change the crop coordinates of a photo and re-crop it."""
index = get_index()
if not key in index["map"]:
raise Exception("Can't crop photo: doesn't exist!")
album = index["map"][key]
# Sanity check.
if not album in index["albums"]:
raise Exception("Can't find photo in album!")
logger.debug("Recropping photo {}".format(key))
# Delete all the images except the large one.
for size in ["thumb", "avatar"]:
pic = index["albums"][album][key][size]
logger.debug("Delete {} size: {}".format(size, pic))
os.unlink(os.path.join(config.PHOTO_ROOT_PRIVATE, pic))
# Regenerate all the thumbnails.
large = index["albums"][album][key]["large"]
source = os.path.join(config.PHOTO_ROOT_PRIVATE, large)
for size in ["thumb", "avatar"]:
pic = resize_photo(source, size, crop=dict(
x=x,
y=y,
length=length,
))
index["albums"][album][key][size] = pic
# Save changes.
write_index(index)
def set_album_cover(album, key):
"""Change the album's cover photo."""
album = sanitize_name(album)
index = get_index()
logger.info("Changing album cover for {} to {}".format(album, key))
if album in index["albums"] and key in index["albums"][album]:
index["covers"][album] = key
write_index(index)
return
logger.error("Failed to change album index! Album or photo not found.")
def edit_photo(key, data):
"""Update a photo's data."""
index = get_index()
if not key in index["map"]:
logger.warning("Tried to delete photo {} but it wasn't found?".format(key))
return
album = index["map"][key]
logger.info("Completely deleting the photo {} from album {}".format(key, album))
index["albums"][album][key].update(data)
write_index(index)
def delete_photo(key):
"""Delete a photo."""
index = get_index()
if not key in index["map"]:
logger.warning("Tried to delete photo {} but it wasn't found?".format(key))
return
album = index["map"][key]
logger.info("Completely deleting the photo {} from album {}".format(key, album))
photo = index["albums"][album][key]
# Delete all the images.
for size in ["large", "thumb", "avatar"]:
logger.info("Delete: {}".format(photo[size]))
os.unlink(os.path.join(config.PHOTO_ROOT_PRIVATE, photo[size]))
# Delete it from the sort list.
index["photo-order"][album].remove(key)
del index["map"][key]
del index["albums"][album][key]
# Was this the album cover?
if index["covers"][album] == key:
# Try to pick a new one.
if len(index["photo-order"][album]) > 0:
index["covers"][album] = index["photo-order"][album][0]
else:
index["covers"][album] = ""
# If the album is empty now too, delete it as well.
if len(index["albums"][album].keys()) == 0:
del index["albums"][album]
del index["photo-order"][album]
del index["covers"][album]
index["album-order"].remove(album)
write_index(index)
def order_albums(order):
"""Reorder the albums according to the new order list."""
index = get_index()
# Sanity check, make sure all albums are included.
if len(order) != len(index["album-order"]):
logger.warning("Can't reorganize albums because the order lists don't match!")
return None
for album in index["album-order"]:
if album not in order:
logger.warning("Tried reorganizing albums, but {} was missing!".format(album))
return None
index["album-order"] = order
write_index(index)
def order_photos(album, order):
"""Reorder the photos according to the new order list."""
index = get_index()
if not album in index["albums"]:
logger.warning("Album not found: {}".format(album))
return None
# Sanity check, make sure all albums are included.
if len(order) != len(index["photo-order"][album]):
logger.warning("Can't reorganize photos because the order lists don't match!")
return None
for key in index["photo-order"][album]:
if key not in order:
logger.warning("Tried reorganizing photos, but {} was missing!".format(key))
return None
index["photo-order"][album] = order
write_index(index)
def upload_from_pc(request):
"""Upload a photo from the user's filesystem.
This requires the Flask `request` object. Returns a dict with the following
keys:
* success: True || False
* error: if unsuccessful
* photo: if successful
"""
form = request.form
upload = request.files["file"]
# Make a temp filename for it.
filetype = upload.filename.rsplit(".", 1)[1]
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype)
logger.debug("Save incoming photo to: {}".format(tempfile))
upload.save(tempfile)
# All good so far. Process the photo.
return process_photo(form, tempfile)
def upload_from_www(form):
"""Upload a photo from the Internet.
This requires the `form` object, but not necessarily Flask's. It just has to
be a dict with the form keys for the upload.
Returns the same structure as `upload_from_pc()`.
"""
url = form.get("url")
if not url or not allowed_filetype(url):
return dict(success=False, error="Invalid file extension.")
# Make a temp filename for it.
filetype = url.rsplit(".", 1)[1]
tempfile = "{}/rophako-photo-{}.{}".format(config.TEMPDIR, int(time.time()), filetype)
logger.debug("Save incoming photo to: {}".format(tempfile))
# Grab the file.
try:
data = requests.get(url).content
except:
return dict(success=False, error="Failed to get that URL.")
fh = open(tempfile, "wb")
fh.write(data)
fh.close()
# All good so far. Process the photo.
return process_photo(form, tempfile)
def process_photo(form, filename):
"""Formats an incoming photo."""
# Resize the photo to each of the various sizes and collect their names.
sizes = dict()
for size in PHOTO_SCALES.keys():
sizes[size] = resize_photo(filename, size)
# Remove the temp file.
os.unlink(filename)
# What album are the photos going to?
album = form.get("album", "")
new_album = form.get("new-album", None)
if album == "" and new_album:
album = new_album
# Sanitize the name.
album = sanitize_name(album)
if album == "":
logger.warning("Album name didn't pass sanitization! Fall back to default album name.")
album = config.PHOTO_DEFAULT_ALBUM
# Make up a unique public key for this set of photos.
key = random_hash()
while photo_exists(key):
key = random_hash()
logger.debug("Photo set public key: {}".format(key))
# Get the album index to manipulate ordering.
index = get_index()
# Update the photo data.
if not album in index["albums"]:
index["albums"][album] = {}
index["albums"][album][key] = dict(
ip=request.remote_addr,
author=g.info["session"]["uid"],
uploaded=int(time.time()),
caption=form.get("caption", ""),
**sizes
)
# Maintain a photo map to album.
index["map"][key] = album
# Add this pic to the front of the album.
if not album in index["photo-order"]:
index["photo-order"][album] = []
index["photo-order"][album].insert(0, key)
# If this is a new album, add it to the front of the album ordering.
if not album in index["album-order"]:
index["album-order"].insert(0, album)
# Set the album cover for a new album.
if not album in index["covers"] or len(index["covers"][album]) == 0:
index["covers"][album] = key
# Save changes to the index.
write_index(index)
return dict(success=True, photo=key)
def allowed_filetype(filename):
"""Query whether the file extension is allowed."""
return "." in filename and \
filename.rsplit(".", 1)[1].lower() in ['jpeg', 'jpe', 'jpg', 'gif', 'png']
def resize_photo(filename, size, crop=None):
"""Resize a photo from the target filename into the requested size.
Optionally the photo can be cropped with custom parameters.
"""
# Find the file type.
filetype = filename.rsplit(".", 1)[1]
if filetype == "jpeg": filetype = "jpg"
# Open the image.
img = Image.open(filename)
# Make up a unique filename.
outfile = random_name(filetype)
target = os.path.join(config.PHOTO_ROOT_PRIVATE, outfile)
logger.debug("Output file for {} scale: {}".format(size, target))
# Get the image's dimensions.
orig_width, orig_height = img.size
new_width = PHOTO_SCALES[size]
logger.debug("Original photo dimensions: {}x{}".format(orig_width, orig_height))
# For the large version, only scale it, don't crop it.
if size == "large":
# Do we NEED to scale it?
if orig_width <= new_width:
logger.debug("Don't need to scale down the large image!")
img.save(target)
return outfile
# Scale it down.
ratio = float(new_width) / float(orig_width)
new_height = int(float(orig_height) * float(ratio))
logger.debug("New image dimensions: {}x{}".format(new_width, new_height))
img = img.resize((new_width, new_height), Image.ANTIALIAS)
img.save(target)
return outfile
# For all other versions, crop them into a square.
x, y, length = 0, 0, 0
# Use 0,0 and find the shortest dimension for the length.
if orig_width > orig_height:
length = orig_height
else:
length = orig_width
# Did they give us crop coordinates?
if crop is not None:
x = crop["x"]
y = crop["y"]
if crop["length"] > 0:
length = crop["length"]
# Adjust the coords if they're impossible.
if x < 0:
logger.warning("X-Coord is less than 0; fixing!")
x = 0
if y < 0:
logger.warning("Y-Coord is less than 0; fixing!")
y = 0
if x > orig_width:
logger.warning("X-Coord is greater than image width; fixing!")
x = orig_width - length
if x < 0: x = 0
if y > orig_height:
logger.warning("Y-Coord is greater than image height; fixing!")
y = orig_height - length
if y < 0: y = 0
# Make sure the crop box fits.
if x + length > orig_width:
diff = x + length - orig_width
logger.warning("Crop box is outside the right edge of the image by {}px; fixing!".format(diff))
length -= diff
if y + length > orig_height:
diff = y + length - orig_height
logger.warning("Crop box is outside the bottom edge of the image by {}px; fixing!".format(diff))
length -= diff
# Do we need to scale?
if new_width == length:
logger.debug("Image doesn't need to be cropped or scaled!")
img.save(target)
return outfile
# Crop to the requested box.
logger.debug("Cropping the photo")
img = img.crop((x, y, x+length, y+length))
# Scale it to the proper dimensions.
img = img.resize((new_width, new_width), Image.ANTIALIAS)
img.save(target)
return outfile
def get_index():
"""Get the photo album index, or a new empty DB if it doesn't exist."""
if JsonDB.exists("photos/index"):
return JsonDB.get("photos/index")
return {
"albums": {}, # Album data
"map": {}, # Map photo keys to albums
"covers": {}, # Album cover photos
"photo-order": {}, # Ordering of photos in albums
"album-order": [], # Ordering of albums themselves
}
def write_index(index):
"""Save the index back to the DB."""
return JsonDB.commit("photos/index", index)
def random_name(filetype):
"""Get a random available file name to save a new photo."""
outfile = random_hash() + "." + filetype
while os.path.isfile(os.path.join(config.PHOTO_ROOT_PRIVATE, outfile)):
outfile = random_hash() + "." + filetype
return outfile
def random_hash():
"""Get a short random hash to use as the base name for a photo."""
md5 = hashlib.md5()
md5.update(str(random.randint(0, 1000000)))
return md5.hexdigest()[:8]

View File

@ -7,6 +7,7 @@ import time
import config import config
import rophako.jsondb as JsonDB import rophako.jsondb as JsonDB
import rophako.model.photo as Photo
from rophako.log import logger from rophako.log import logger
def create(username, password, name=None, uid=None, role="user"): def create(username, password, name=None, uid=None, role="user"):
@ -47,6 +48,7 @@ def create(username, password, name=None, uid=None, role="user"):
uid=uid, uid=uid,
username=username, username=username,
name=name, name=name,
picture="",
role=role, role=role,
password=hashedpass, password=hashedpass,
created=time.time(), created=time.time(),
@ -125,6 +127,17 @@ def get_user(uid=None, username=None):
return JsonDB.get("users/by-id/{}".format(uid)) return JsonDB.get("users/by-id/{}".format(uid))
def get_picture(uid):
"""Get the chosen profile photo for the user."""
data = get_user(uid=uid)
pic = data["picture"]
if len(pic):
photo = Photo.get_photo(pic)
if photo:
return photo["avatar"]
return None
def exists(uid=None, username=None): def exists(uid=None, username=None):
"""Query whether a user ID or name exists.""" """Query whether a user ID or name exists."""
if uid: if uid:

View File

@ -9,7 +9,7 @@ import calendar
import rophako.model.user as User import rophako.model.user as User
import rophako.model.blog as Blog import rophako.model.blog as Blog
from rophako.utils import template, pretty_time, admin_required from rophako.utils import template, pretty_time, login_required
from rophako.log import logger from rophako.log import logger
from config import * from config import *
@ -42,6 +42,8 @@ def entry(fid):
# Get the author's information. # Get the author's information.
post["profile"] = User.get_user(uid=post["author"]) post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"])
post["photo_url"] = PHOTO_ROOT_PUBLIC
# Pretty-print the time. # Pretty-print the time.
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"]) post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
@ -60,7 +62,7 @@ def dummy():
@mod.route("/update", methods=["GET", "POST"]) @mod.route("/update", methods=["GET", "POST"])
@admin_required @login_required
def update(): def update():
"""Post/edit a blog entry.""" """Post/edit a blog entry."""
@ -192,7 +194,7 @@ def update():
@mod.route("/delete", methods=["GET", "POST"]) @mod.route("/delete", methods=["GET", "POST"])
@admin_required @login_required
def delete(): def delete():
"""Delete a blog post.""" """Delete a blog post."""
post_id = request.args.get("id") post_id = request.args.get("id")
@ -286,6 +288,8 @@ def partial_index():
# Get the author's information. # Get the author's information.
post["profile"] = User.get_user(uid=post["author"]) post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"])
post["photo_url"] = PHOTO_ROOT_PUBLIC
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"]) post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])

246
rophako/modules/photo.py Normal file
View File

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
"""Endpoints for the photo albums."""
from flask import Blueprint, g, request, redirect, url_for, session, flash
import rophako.model.user as User
import rophako.model.photo as Photo
from rophako.utils import template, pretty_time, login_required
from rophako.log import logger
from config import *
mod = Blueprint("photo", __name__, url_prefix="/photos")
@mod.route("/")
def index():
return redirect(url_for(".albums"))
@mod.route("/albums")
def albums():
"""View the index of the photo albums."""
albums = Photo.list_albums()
# If there's only one album, jump directly to that one.
if len(albums) == 1:
return redirect(url_for(".album_index", name=albums[0]["name"]))
g.info["albums"] = albums
return template("photos/albums.html")
@mod.route("/album/<name>")
def album_index(name):
"""View the photos inside an album."""
photos = Photo.list_photos(name)
if photos is None:
flash("That album doesn't exist.")
return redirect(url_for(".albums"))
g.info["album"] = name
g.info["photos"] = photos
return template("photos/album.html")
@mod.route("/view/<key>")
def view_photo(key):
"""View a specific photo."""
photo = Photo.get_photo(key)
if photo is None:
flash("That photo wasn't found!")
return redirect(url_for(".albums"))
from pprint import pprint
pprint(photo)
# Get the author info.
author = User.get_user(uid=photo["author"])
if author:
g.info["author"] = author
g.info["photo"] = photo
g.info["photo"]["key"] = key
g.info["photo"]["pretty_time"] = pretty_time(PHOTO_TIME_FORMAT, photo["uploaded"])
return template("photos/view.html")
@mod.route("/upload", methods=["GET", "POST"])
@login_required
def upload():
"""Upload a photo."""
if request.method == "POST":
# We're posting the upload. What source is the pic from?
result = None
location = request.form.get("location")
if location == "pc":
# An upload from the PC.
result = Photo.upload_from_pc(request)
elif location == "www":
# An upload from the Internet.
result = Photo.upload_from_www(request.form)
else:
flash("Stop messing around.")
return redirect(url_for(".upload"))
# How'd it go?
if result["success"] is not True:
flash("The upload has failed: {}".format(result["error"]))
return redirect(url_for(".upload"))
return redirect(url_for(".crop", photo=result["photo"]))
# Get the list of available albums.
g.info["album_list"] = [
"My Photos", # the default
]
g.info["selected"] = PHOTO_DEFAULT_ALBUM
albums = Photo.list_albums()
if len(albums):
g.info["album_list"] = [ album["name"] for album in albums ]
g.info["selected"] = albums[0]
return template("photos/upload.html")
@mod.route("/crop/<photo>", methods=["GET", "POST"])
@login_required
def crop(photo):
pic = Photo.get_photo(photo)
if not pic:
flash("The photo you want to crop wasn't found!")
return redirect(url_for(".albums"))
# Saving?
if request.method == "POST":
try:
x = int(request.form.get("x", 0))
y = int(request.form.get("y", 0))
length = int(request.form.get("length", 0))
except:
flash("Error with form inputs.")
return redirect(url_for(".crop", photo=photo))
# Re-crop the photo!
Photo.crop_photo(photo, x, y, length)
flash("The photo has been cropped!")
return redirect(url_for(".albums")) # TODO go to photo
# Get the photo's true size.
true_width, true_height = Photo.get_image_dimensions(pic)
g.info["true_width"] = true_width
g.info["true_height"] = true_height
g.info["photo"] = photo
g.info["preview"] = pic["large"]
return template("photos/crop.html")
@mod.route("/set_cover/<album>/<key>")
@login_required
def set_cover(album, key):
"""Set the pic as the album cover."""
pic = Photo.get_photo(key)
if not pic:
flash("The photo you want to crop wasn't found!")
return redirect(url_for(".albums"))
Photo.set_album_cover(album, key)
flash("Album cover has been set.")
return redirect(url_for(".albums"))
@mod.route("/set_profile/<key>")
@login_required
def set_profile(key):
"""Set the pic as your profile picture."""
pic = Photo.get_photo(key)
if not pic:
flash("The photo wasn't found!")
return redirect(url_for(".albums"))
uid = g.info["session"]["uid"]
User.update_user(uid, dict(picture=key))
flash("Your profile picture has been updated.")
return redirect(url_for(".view_photo", key=key))
@mod.route("/edit/<key>", methods=["GET", "POST"])
@login_required
def edit(key):
"""Edit a photo."""
pic = Photo.get_photo(key)
if not pic:
flash("The photo wasn't found!")
return redirect(url_for(".albums"))
if request.method == "POST":
caption = request.form.get("caption", "")
Photo.edit_photo(key, dict(caption=caption))
flash("The photo has been updated.")
return redirect(url_for(".view_photo", key=key))
g.info["key"] = key
g.info["photo"] = pic
return template("photos/edit.html")
@mod.route("/delete/<key>", methods=["GET", "POST"])
@login_required
def delete(key):
"""Delete a photo."""
pic = Photo.get_photo(key)
if not pic:
flash("The photo wasn't found!")
return redirect(url_for(".albums"))
if request.method == "POST":
# Do it.
Photo.delete_photo(key)
flash("The photo has been deleted.")
return redirect(url_for(".albums"))
g.info["key"] = key
g.info["photo"] = pic
return template("photos/delete.html")
@mod.route("/arrange_albums", methods=["GET", "POST"])
@login_required
def arrange_albums():
"""Rearrange the photo album order."""
albums = Photo.list_albums()
if len(albums) == 0:
flash("There are no albums yet.")
return redirect(url_for(".albums"))
if request.method == "POST":
order = request.form.get("order", "").split(";")
print order
Photo.order_albums(order)
flash("The albums have been rearranged!")
return redirect(url_for(".albums"))
g.info["albums"] = albums
return template("photos/arrange_albums.html")
@mod.route("/arrange_photos/<album>", methods=["GET", "POST"])
@login_required
def arrange_photos(album):
"""Rearrange the photos in an album."""
photos = Photo.list_photos(album)
if photos is None:
flash("That album doesn't exist.")
return redirect(url_for(".albums"))
if request.method == "POST":
order = request.form.get("order", "").split(";")
Photo.order_photos(album, order)
flash("The albums have been rearranged!")
return redirect(url_for(".album_index", name=album))
g.info["album"] = album
g.info["photos"] = photos
return template("photos/arrange_photos.html")

View File

@ -76,3 +76,10 @@ def pretty_time(time_format, unix):
"""Pretty-print a time stamp.""" """Pretty-print a time stamp."""
date = datetime.datetime.fromtimestamp(unix) date = datetime.datetime.fromtimestamp(unix)
return date.strftime(time_format) return date.strftime(time_format)
def sanitize_name(name):
"""Sanitize a name that may be used in the filesystem.
Only allows numbers, letters, and some symbols."""
return re.sub(r'[^A-Za-z0-9 .-_]+', '', name)

View File

@ -10,7 +10,9 @@
<div class="blog-author"> <div class="blog-author">
<a href="#">{# TODO #} <a href="#">{# TODO #}
{% if post["avatar"] %} {% if post["photo"] %}
<img src="{{ post['photo_url'] }}/{{ post['photo'] }}">
{% elif post["avatar"] %}
<img src="/static/avatars/{{ post['avatar'] }}"> <img src="/static/avatars/{{ post['avatar'] }}">
{% else %} {% else %}
<img src="/static/avatars/default.png"> <img src="/static/avatars/default.png">

BIN
rophako/www/css/Jcrop.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,165 @@
/* jquery.Jcrop.css v0.9.12 - MIT License */
/*
The outer-most container in a typical Jcrop instance
If you are having difficulty with formatting related to styles
on a parent element, place any fixes here or in a like selector
You can also style this element if you want to add a border, etc
A better method for styling can be seen below with .jcrop-light
(Add a class to the holder and style elements for that extended class)
*/
.jcrop-holder {
direction: ltr;
text-align: left;
}
/* Selection Border */
.jcrop-vline,
.jcrop-hline {
background: #ffffff url("Jcrop.gif");
font-size: 0;
position: absolute;
}
.jcrop-vline {
height: 100%;
width: 1px !important;
}
.jcrop-vline.right {
right: 0;
}
.jcrop-hline {
height: 1px !important;
width: 100%;
}
.jcrop-hline.bottom {
bottom: 0;
}
/* Invisible click targets */
.jcrop-tracker {
height: 100%;
width: 100%;
/* "turn off" link highlight */
-webkit-tap-highlight-color: transparent;
/* disable callout, image save panel */
-webkit-touch-callout: none;
/* disable cut copy paste */
-webkit-user-select: none;
}
/* Selection Handles */
.jcrop-handle {
background-color: #333333;
border: 1px #eeeeee solid;
width: 7px;
height: 7px;
font-size: 1px;
}
.jcrop-handle.ord-n {
left: 50%;
margin-left: -4px;
margin-top: -4px;
top: 0;
}
.jcrop-handle.ord-s {
bottom: 0;
left: 50%;
margin-bottom: -4px;
margin-left: -4px;
}
.jcrop-handle.ord-e {
margin-right: -4px;
margin-top: -4px;
right: 0;
top: 50%;
}
.jcrop-handle.ord-w {
left: 0;
margin-left: -4px;
margin-top: -4px;
top: 50%;
}
.jcrop-handle.ord-nw {
left: 0;
margin-left: -4px;
margin-top: -4px;
top: 0;
}
.jcrop-handle.ord-ne {
margin-right: -4px;
margin-top: -4px;
right: 0;
top: 0;
}
.jcrop-handle.ord-se {
bottom: 0;
margin-bottom: -4px;
margin-right: -4px;
right: 0;
}
.jcrop-handle.ord-sw {
bottom: 0;
left: 0;
margin-bottom: -4px;
margin-left: -4px;
}
/* Dragbars */
.jcrop-dragbar.ord-n,
.jcrop-dragbar.ord-s {
height: 7px;
width: 100%;
}
.jcrop-dragbar.ord-e,
.jcrop-dragbar.ord-w {
height: 100%;
width: 7px;
}
.jcrop-dragbar.ord-n {
margin-top: -4px;
}
.jcrop-dragbar.ord-s {
bottom: 0;
margin-bottom: -4px;
}
.jcrop-dragbar.ord-e {
margin-right: -4px;
right: 0;
}
.jcrop-dragbar.ord-w {
margin-left: -4px;
}
/* The "jcrop-light" class/extension */
.jcrop-light .jcrop-vline,
.jcrop-light .jcrop-hline {
background: #ffffff;
filter: alpha(opacity=70) !important;
opacity: .70!important;
}
.jcrop-light .jcrop-handle {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: #000000;
border-color: #ffffff;
border-radius: 3px;
}
/* The "jcrop-dark" class/extension */
.jcrop-dark .jcrop-vline,
.jcrop-dark .jcrop-hline {
background: #000000;
filter: alpha(opacity=70) !important;
opacity: 0.7 !important;
}
.jcrop-dark .jcrop-handle {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: #ffffff;
border-color: #000000;
border-radius: 3px;
}
/* Simple macro to turn off the antlines */
.solid-line .jcrop-vline,
.solid-line .jcrop-hline {
background: #ffffff;
}
/* Fix for twitter bootstrap et al. */
.jcrop-holder img,
img.jcrop-preview {
max-width: none;
}

29
rophako/www/css/jquery.Jcrop.min.css vendored Normal file
View File

@ -0,0 +1,29 @@
/* jquery.Jcrop.min.css v0.9.12 (build:20130126) */
.jcrop-holder{direction:ltr;text-align:left;}
.jcrop-vline,.jcrop-hline{background:#FFF url(Jcrop.gif);font-size:0;position:absolute;}
.jcrop-vline{height:100%;width:1px!important;}
.jcrop-vline.right{right:0;}
.jcrop-hline{height:1px!important;width:100%;}
.jcrop-hline.bottom{bottom:0;}
.jcrop-tracker{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;height:100%;width:100%;}
.jcrop-handle{background-color:#333;border:1px #EEE solid;font-size:1px;height:7px;width:7px;}
.jcrop-handle.ord-n{left:50%;margin-left:-4px;margin-top:-4px;top:0;}
.jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-4px;margin-left:-4px;}
.jcrop-handle.ord-e{margin-right:-4px;margin-top:-4px;right:0;top:50%;}
.jcrop-handle.ord-w{left:0;margin-left:-4px;margin-top:-4px;top:50%;}
.jcrop-handle.ord-nw{left:0;margin-left:-4px;margin-top:-4px;top:0;}
.jcrop-handle.ord-ne{margin-right:-4px;margin-top:-4px;right:0;top:0;}
.jcrop-handle.ord-se{bottom:0;margin-bottom:-4px;margin-right:-4px;right:0;}
.jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-4px;margin-left:-4px;}
.jcrop-dragbar.ord-n,.jcrop-dragbar.ord-s{height:7px;width:100%;}
.jcrop-dragbar.ord-e,.jcrop-dragbar.ord-w{height:100%;width:7px;}
.jcrop-dragbar.ord-n{margin-top:-4px;}
.jcrop-dragbar.ord-s{bottom:0;margin-bottom:-4px;}
.jcrop-dragbar.ord-e{margin-right:-4px;right:0;}
.jcrop-dragbar.ord-w{margin-left:-4px;}
.jcrop-light .jcrop-vline,.jcrop-light .jcrop-hline{background:#FFF;filter:alpha(opacity=70)!important;opacity:.70!important;}
.jcrop-light .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#000;border-color:#FFF;border-radius:3px;}
.jcrop-dark .jcrop-vline,.jcrop-dark .jcrop-hline{background:#000;filter:alpha(opacity=70)!important;opacity:.7!important;}
.jcrop-dark .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#FFF;border-color:#000;border-radius:3px;}
.solid-line .jcrop-vline,.solid-line .jcrop-hline{background:#FFF;}
.jcrop-holder img,img.jcrop-preview{max-width:none;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,485 @@
/*! jQuery UI - v1.10.4 - 2014-04-05
* http://jqueryui.com
* Includes: jquery.ui.core.css, jquery.ui.theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
/* Layout helpers
----------------------------------*/
.ui-helper-hidden {
display: none;
}
.ui-helper-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.ui-helper-reset {
margin: 0;
padding: 0;
border: 0;
outline: 0;
line-height: 1.3;
text-decoration: none;
font-size: 100%;
list-style: none;
}
.ui-helper-clearfix:before,
.ui-helper-clearfix:after {
content: "";
display: table;
border-collapse: collapse;
}
.ui-helper-clearfix:after {
clear: both;
}
.ui-helper-clearfix {
min-height: 0; /* support: IE7 */
}
.ui-helper-zfix {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
opacity: 0;
filter:Alpha(Opacity=0);
}
.ui-front {
z-index: 100;
}
/* Interaction Cues
----------------------------------*/
.ui-state-disabled {
cursor: default !important;
}
/* Icons
----------------------------------*/
/* states and images */
.ui-icon {
display: block;
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
}
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
font-size: 1.1em;
}
.ui-widget .ui-widget {
font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
font-size: 1em;
}
.ui-widget-content {
border: 1px solid #dddddd;
background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x;
color: #333333;
}
.ui-widget-content a {
color: #333333;
}
.ui-widget-header {
border: 1px solid #e78f08;
background: #f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x;
color: #ffffff;
font-weight: bold;
}
.ui-widget-header a {
color: #ffffff;
}
/* Interaction states
----------------------------------*/
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
border: 1px solid #cccccc;
background: #f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x;
font-weight: bold;
color: #1c94c4;
}
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited {
color: #1c94c4;
text-decoration: none;
}
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
.ui-state-focus,
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus {
border: 1px solid #fbcb09;
background: #fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x;
font-weight: bold;
color: #c77405;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited,
.ui-state-focus a,
.ui-state-focus a:hover,
.ui-state-focus a:link,
.ui-state-focus a:visited {
color: #c77405;
text-decoration: none;
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active {
border: 1px solid #fbd850;
background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;
font-weight: bold;
color: #eb8f00;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
color: #eb8f00;
text-decoration: none;
}
/* Interaction Cues
----------------------------------*/
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
border: 1px solid #fed22f;
background: #ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x;
color: #363636;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
color: #363636;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
border: 1px solid #cd0a0a;
background: #b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat;
color: #ffffff;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
color: #ffffff;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
color: #ffffff;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
opacity: .7;
filter:Alpha(Opacity=70);
font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
opacity: .35;
filter:Alpha(Opacity=35);
background-image: none;
}
.ui-state-disabled .ui-icon {
filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
}
/* Icons
----------------------------------*/
/* states and images */
.ui-icon {
width: 16px;
height: 16px;
}
.ui-icon,
.ui-widget-content .ui-icon {
background-image: url(images/ui-icons_222222_256x240.png);
}
.ui-widget-header .ui-icon {
background-image: url(images/ui-icons_ffffff_256x240.png);
}
.ui-state-default .ui-icon {
background-image: url(images/ui-icons_ef8c08_256x240.png);
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon {
background-image: url(images/ui-icons_ef8c08_256x240.png);
}
.ui-state-active .ui-icon {
background-image: url(images/ui-icons_ef8c08_256x240.png);
}
.ui-state-highlight .ui-icon {
background-image: url(images/ui-icons_228ef1_256x240.png);
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
background-image: url(images/ui-icons_ffd27a_256x240.png);
}
/* positioning */
.ui-icon-blank { background-position: 16px 16px; }
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
border-top-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
border-top-right-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
border-bottom-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
border-bottom-right-radius: 4px;
}
/* Overlays */
.ui-widget-overlay {
background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat;
opacity: .5;
filter: Alpha(Opacity=50);
}
.ui-widget-shadow {
margin: -5px 0 0 -5px;
padding: 5px;
background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x;
opacity: .2;
filter: Alpha(Opacity=20);
border-radius: 5px;
}

File diff suppressed because one or more lines are too long

2747
rophako/www/js/jquery-ui-1.10.4.custom.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

22
rophako/www/js/jquery.Jcrop.min.js vendored Normal file
View File

@ -0,0 +1,22 @@
/**
* jquery.Jcrop.min.js v0.9.12 (build:20130202)
* jQuery Image Cropping Plugin - released under MIT License
* Copyright (c) 2008-2013 Tapmodo Interactive LLC
* https://github.com/tapmodo/Jcrop
*/
(function(a){a.Jcrop=function(b,c){function i(a){return Math.round(a)+"px"}function j(a){return d.baseClass+"-"+a}function k(){return a.fx.step.hasOwnProperty("backgroundColor")}function l(b){var c=a(b).offset();return[c.left,c.top]}function m(a){return[a.pageX-e[0],a.pageY-e[1]]}function n(b){typeof b!="object"&&(b={}),d=a.extend(d,b),a.each(["onChange","onSelect","onRelease","onDblClick"],function(a,b){typeof d[b]!="function"&&(d[b]=function(){})})}function o(a,b,c){e=l(D),bc.setCursor(a==="move"?a:a+"-resize");if(a==="move")return bc.activateHandlers(q(b),v,c);var d=_.getFixed(),f=r(a),g=_.getCorner(r(f));_.setPressed(_.getCorner(f)),_.setCurrent(g),bc.activateHandlers(p(a,d),v,c)}function p(a,b){return function(c){if(!d.aspectRatio)switch(a){case"e":c[1]=b.y2;break;case"w":c[1]=b.y2;break;case"n":c[0]=b.x2;break;case"s":c[0]=b.x2}else switch(a){case"e":c[1]=b.y+1;break;case"w":c[1]=b.y+1;break;case"n":c[0]=b.x+1;break;case"s":c[0]=b.x+1}_.setCurrent(c),bb.update()}}function q(a){var b=a;return bd.watchKeys
(),function(a){_.moveOffset([a[0]-b[0],a[1]-b[1]]),b=a,bb.update()}}function r(a){switch(a){case"n":return"sw";case"s":return"nw";case"e":return"nw";case"w":return"ne";case"ne":return"sw";case"nw":return"se";case"se":return"nw";case"sw":return"ne"}}function s(a){return function(b){return d.disabled?!1:a==="move"&&!d.allowMove?!1:(e=l(D),W=!0,o(a,m(b)),b.stopPropagation(),b.preventDefault(),!1)}}function t(a,b,c){var d=a.width(),e=a.height();d>b&&b>0&&(d=b,e=b/a.width()*a.height()),e>c&&c>0&&(e=c,d=c/a.height()*a.width()),T=a.width()/d,U=a.height()/e,a.width(d).height(e)}function u(a){return{x:a.x*T,y:a.y*U,x2:a.x2*T,y2:a.y2*U,w:a.w*T,h:a.h*U}}function v(a){var b=_.getFixed();b.w>d.minSelect[0]&&b.h>d.minSelect[1]?(bb.enableHandles(),bb.done()):bb.release(),bc.setCursor(d.allowSelect?"crosshair":"default")}function w(a){if(d.disabled)return!1;if(!d.allowSelect)return!1;W=!0,e=l(D),bb.disableHandles(),bc.setCursor("crosshair");var b=m(a);return _.setPressed(b),bb.update(),bc.activateHandlers(x,v,a.type.substring
(0,5)==="touch"),bd.watchKeys(),a.stopPropagation(),a.preventDefault(),!1}function x(a){_.setCurrent(a),bb.update()}function y(){var b=a("<div></div>").addClass(j("tracker"));return g&&b.css({opacity:0,backgroundColor:"white"}),b}function be(a){G.removeClass().addClass(j("holder")).addClass(a)}function bf(a,b){function t(){window.setTimeout(u,l)}var c=a[0]/T,e=a[1]/U,f=a[2]/T,g=a[3]/U;if(X)return;var h=_.flipCoords(c,e,f,g),i=_.getFixed(),j=[i.x,i.y,i.x2,i.y2],k=j,l=d.animationDelay,m=h[0]-j[0],n=h[1]-j[1],o=h[2]-j[2],p=h[3]-j[3],q=0,r=d.swingSpeed;c=k[0],e=k[1],f=k[2],g=k[3],bb.animMode(!0);var s,u=function(){return function(){q+=(100-q)/r,k[0]=Math.round(c+q/100*m),k[1]=Math.round(e+q/100*n),k[2]=Math.round(f+q/100*o),k[3]=Math.round(g+q/100*p),q>=99.8&&(q=100),q<100?(bh(k),t()):(bb.done(),bb.animMode(!1),typeof b=="function"&&b.call(bs))}}();t()}function bg(a){bh([a[0]/T,a[1]/U,a[2]/T,a[3]/U]),d.onSelect.call(bs,u(_.getFixed())),bb.enableHandles()}function bh(a){_.setPressed([a[0],a[1]]),_.setCurrent([a[2],
a[3]]),bb.update()}function bi(){return u(_.getFixed())}function bj(){return _.getFixed()}function bk(a){n(a),br()}function bl(){d.disabled=!0,bb.disableHandles(),bb.setCursor("default"),bc.setCursor("default")}function bm(){d.disabled=!1,br()}function bn(){bb.done(),bc.activateHandlers(null,null)}function bo(){G.remove(),A.show(),A.css("visibility","visible"),a(b).removeData("Jcrop")}function bp(a,b){bb.release(),bl();var c=new Image;c.onload=function(){var e=c.width,f=c.height,g=d.boxWidth,h=d.boxHeight;D.width(e).height(f),D.attr("src",a),H.attr("src",a),t(D,g,h),E=D.width(),F=D.height(),H.width(E).height(F),M.width(E+L*2).height(F+L*2),G.width(E).height(F),ba.resize(E,F),bm(),typeof b=="function"&&b.call(bs)},c.src=a}function bq(a,b,c){var e=b||d.bgColor;d.bgFade&&k()&&d.fadeTime&&!c?a.animate({backgroundColor:e},{queue:!1,duration:d.fadeTime}):a.css("backgroundColor",e)}function br(a){d.allowResize?a?bb.enableOnly():bb.enableHandles():bb.disableHandles(),bc.setCursor(d.allowSelect?"crosshair":"default"),bb
.setCursor(d.allowMove?"move":"default"),d.hasOwnProperty("trueSize")&&(T=d.trueSize[0]/E,U=d.trueSize[1]/F),d.hasOwnProperty("setSelect")&&(bg(d.setSelect),bb.done(),delete d.setSelect),ba.refresh(),d.bgColor!=N&&(bq(d.shade?ba.getShades():G,d.shade?d.shadeColor||d.bgColor:d.bgColor),N=d.bgColor),O!=d.bgOpacity&&(O=d.bgOpacity,d.shade?ba.refresh():bb.setBgOpacity(O)),P=d.maxSize[0]||0,Q=d.maxSize[1]||0,R=d.minSize[0]||0,S=d.minSize[1]||0,d.hasOwnProperty("outerImage")&&(D.attr("src",d.outerImage),delete d.outerImage),bb.refresh()}var d=a.extend({},a.Jcrop.defaults),e,f=navigator.userAgent.toLowerCase(),g=/msie/.test(f),h=/msie [1-6]\./.test(f);typeof b!="object"&&(b=a(b)[0]),typeof c!="object"&&(c={}),n(c);var z={border:"none",visibility:"visible",margin:0,padding:0,position:"absolute",top:0,left:0},A=a(b),B=!0;if(b.tagName=="IMG"){if(A[0].width!=0&&A[0].height!=0)A.width(A[0].width),A.height(A[0].height);else{var C=new Image;C.src=A[0].src,A.width(C.width),A.height(C.height)}var D=A.clone().removeAttr("id").
css(z).show();D.width(A.width()),D.height(A.height()),A.after(D).hide()}else D=A.css(z).show(),B=!1,d.shade===null&&(d.shade=!0);t(D,d.boxWidth,d.boxHeight);var E=D.width(),F=D.height(),G=a("<div />").width(E).height(F).addClass(j("holder")).css({position:"relative",backgroundColor:d.bgColor}).insertAfter(A).append(D);d.addClass&&G.addClass(d.addClass);var H=a("<div />"),I=a("<div />").width("100%").height("100%").css({zIndex:310,position:"absolute",overflow:"hidden"}),J=a("<div />").width("100%").height("100%").css("zIndex",320),K=a("<div />").css({position:"absolute",zIndex:600}).dblclick(function(){var a=_.getFixed();d.onDblClick.call(bs,a)}).insertBefore(D).append(I,J);B&&(H=a("<img />").attr("src",D.attr("src")).css(z).width(E).height(F),I.append(H)),h&&K.css({overflowY:"hidden"});var L=d.boundary,M=y().width(E+L*2).height(F+L*2).css({position:"absolute",top:i(-L),left:i(-L),zIndex:290}).mousedown(w),N=d.bgColor,O=d.bgOpacity,P,Q,R,S,T,U,V=!0,W,X,Y;e=l(D);var Z=function(){function a(){var a={},b=["touchstart"
,"touchmove","touchend"],c=document.createElement("div"),d;try{for(d=0;d<b.length;d++){var e=b[d];e="on"+e;var f=e in c;f||(c.setAttribute(e,"return;"),f=typeof c[e]=="function"),a[b[d]]=f}return a.touchstart&&a.touchend&&a.touchmove}catch(g){return!1}}function b(){return d.touchSupport===!0||d.touchSupport===!1?d.touchSupport:a()}return{createDragger:function(a){return function(b){return d.disabled?!1:a==="move"&&!d.allowMove?!1:(e=l(D),W=!0,o(a,m(Z.cfilter(b)),!0),b.stopPropagation(),b.preventDefault(),!1)}},newSelection:function(a){return w(Z.cfilter(a))},cfilter:function(a){return a.pageX=a.originalEvent.changedTouches[0].pageX,a.pageY=a.originalEvent.changedTouches[0].pageY,a},isSupported:a,support:b()}}(),_=function(){function h(d){d=n(d),c=a=d[0],e=b=d[1]}function i(a){a=n(a),f=a[0]-c,g=a[1]-e,c=a[0],e=a[1]}function j(){return[f,g]}function k(d){var f=d[0],g=d[1];0>a+f&&(f-=f+a),0>b+g&&(g-=g+b),F<e+g&&(g+=F-(e+g)),E<c+f&&(f+=E-(c+f)),a+=f,c+=f,b+=g,e+=g}function l(a){var b=m();switch(a){case"ne":return[
b.x2,b.y];case"nw":return[b.x,b.y];case"se":return[b.x2,b.y2];case"sw":return[b.x,b.y2]}}function m(){if(!d.aspectRatio)return p();var f=d.aspectRatio,g=d.minSize[0]/T,h=d.maxSize[0]/T,i=d.maxSize[1]/U,j=c-a,k=e-b,l=Math.abs(j),m=Math.abs(k),n=l/m,r,s,t,u;return h===0&&(h=E*10),i===0&&(i=F*10),n<f?(s=e,t=m*f,r=j<0?a-t:t+a,r<0?(r=0,u=Math.abs((r-a)/f),s=k<0?b-u:u+b):r>E&&(r=E,u=Math.abs((r-a)/f),s=k<0?b-u:u+b)):(r=c,u=l/f,s=k<0?b-u:b+u,s<0?(s=0,t=Math.abs((s-b)*f),r=j<0?a-t:t+a):s>F&&(s=F,t=Math.abs(s-b)*f,r=j<0?a-t:t+a)),r>a?(r-a<g?r=a+g:r-a>h&&(r=a+h),s>b?s=b+(r-a)/f:s=b-(r-a)/f):r<a&&(a-r<g?r=a-g:a-r>h&&(r=a-h),s>b?s=b+(a-r)/f:s=b-(a-r)/f),r<0?(a-=r,r=0):r>E&&(a-=r-E,r=E),s<0?(b-=s,s=0):s>F&&(b-=s-F,s=F),q(o(a,b,r,s))}function n(a){return a[0]<0&&(a[0]=0),a[1]<0&&(a[1]=0),a[0]>E&&(a[0]=E),a[1]>F&&(a[1]=F),[Math.round(a[0]),Math.round(a[1])]}function o(a,b,c,d){var e=a,f=c,g=b,h=d;return c<a&&(e=c,f=a),d<b&&(g=d,h=b),[e,g,f,h]}function p(){var d=c-a,f=e-b,g;return P&&Math.abs(d)>P&&(c=d>0?a+P:a-P),Q&&Math.abs
(f)>Q&&(e=f>0?b+Q:b-Q),S/U&&Math.abs(f)<S/U&&(e=f>0?b+S/U:b-S/U),R/T&&Math.abs(d)<R/T&&(c=d>0?a+R/T:a-R/T),a<0&&(c-=a,a-=a),b<0&&(e-=b,b-=b),c<0&&(a-=c,c-=c),e<0&&(b-=e,e-=e),c>E&&(g=c-E,a-=g,c-=g),e>F&&(g=e-F,b-=g,e-=g),a>E&&(g=a-F,e-=g,b-=g),b>F&&(g=b-F,e-=g,b-=g),q(o(a,b,c,e))}function q(a){return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]}}var a=0,b=0,c=0,e=0,f,g;return{flipCoords:o,setPressed:h,setCurrent:i,getOffset:j,moveOffset:k,getCorner:l,getFixed:m}}(),ba=function(){function f(a,b){e.left.css({height:i(b)}),e.right.css({height:i(b)})}function g(){return h(_.getFixed())}function h(a){e.top.css({left:i(a.x),width:i(a.w),height:i(a.y)}),e.bottom.css({top:i(a.y2),left:i(a.x),width:i(a.w),height:i(F-a.y2)}),e.right.css({left:i(a.x2),width:i(E-a.x2)}),e.left.css({width:i(a.x)})}function j(){return a("<div />").css({position:"absolute",backgroundColor:d.shadeColor||d.bgColor}).appendTo(c)}function k(){b||(b=!0,c.insertBefore(D),g(),bb.setBgOpacity(1,0,1),H.hide(),l(d.shadeColor||d.bgColor,1),bb.
isAwake()?n(d.bgOpacity,1):n(1,1))}function l(a,b){bq(p(),a,b)}function m(){b&&(c.remove(),H.show(),b=!1,bb.isAwake()?bb.setBgOpacity(d.bgOpacity,1,1):(bb.setBgOpacity(1,1,1),bb.disableHandles()),bq(G,0,1))}function n(a,e){b&&(d.bgFade&&!e?c.animate({opacity:1-a},{queue:!1,duration:d.fadeTime}):c.css({opacity:1-a}))}function o(){d.shade?k():m(),bb.isAwake()&&n(d.bgOpacity)}function p(){return c.children()}var b=!1,c=a("<div />").css({position:"absolute",zIndex:240,opacity:0}),e={top:j(),left:j().height(F),right:j().height(F),bottom:j()};return{update:g,updateRaw:h,getShades:p,setBgColor:l,enable:k,disable:m,resize:f,refresh:o,opacity:n}}(),bb=function(){function k(b){var c=a("<div />").css({position:"absolute",opacity:d.borderOpacity}).addClass(j(b));return I.append(c),c}function l(b,c){var d=a("<div />").mousedown(s(b)).css({cursor:b+"-resize",position:"absolute",zIndex:c}).addClass("ord-"+b);return Z.support&&d.bind("touchstart.jcrop",Z.createDragger(b)),J.append(d),d}function m(a){var b=d.handleSize,e=l(a,c++
).css({opacity:d.handleOpacity}).addClass(j("handle"));return b&&e.width(b).height(b),e}function n(a){return l(a,c++).addClass("jcrop-dragbar")}function o(a){var b;for(b=0;b<a.length;b++)g[a[b]]=n(a[b])}function p(a){var b,c;for(c=0;c<a.length;c++){switch(a[c]){case"n":b="hline";break;case"s":b="hline bottom";break;case"e":b="vline right";break;case"w":b="vline"}e[a[c]]=k(b)}}function q(a){var b;for(b=0;b<a.length;b++)f[a[b]]=m(a[b])}function r(a,b){d.shade||H.css({top:i(-b),left:i(-a)}),K.css({top:i(b),left:i(a)})}function t(a,b){K.width(Math.round(a)).height(Math.round(b))}function v(){var a=_.getFixed();_.setPressed([a.x,a.y]),_.setCurrent([a.x2,a.y2]),w()}function w(a){if(b)return x(a)}function x(a){var c=_.getFixed();t(c.w,c.h),r(c.x,c.y),d.shade&&ba.updateRaw(c),b||A(),a?d.onSelect.call(bs,u(c)):d.onChange.call(bs,u(c))}function z(a,c,e){if(!b&&!c)return;d.bgFade&&!e?D.animate({opacity:a},{queue:!1,duration:d.fadeTime}):D.css("opacity",a)}function A(){K.show(),d.shade?ba.opacity(O):z(O,!0),b=!0}function B
(){F(),K.hide(),d.shade?ba.opacity(1):z(1),b=!1,d.onRelease.call(bs)}function C(){h&&J.show()}function E(){h=!0;if(d.allowResize)return J.show(),!0}function F(){h=!1,J.hide()}function G(a){a?(X=!0,F()):(X=!1,E())}function L(){G(!1),v()}var b,c=370,e={},f={},g={},h=!1;d.dragEdges&&a.isArray(d.createDragbars)&&o(d.createDragbars),a.isArray(d.createHandles)&&q(d.createHandles),d.drawBorders&&a.isArray(d.createBorders)&&p(d.createBorders),a(document).bind("touchstart.jcrop-ios",function(b){a(b.currentTarget).hasClass("jcrop-tracker")&&b.stopPropagation()});var M=y().mousedown(s("move")).css({cursor:"move",position:"absolute",zIndex:360});return Z.support&&M.bind("touchstart.jcrop",Z.createDragger("move")),I.append(M),F(),{updateVisible:w,update:x,release:B,refresh:v,isAwake:function(){return b},setCursor:function(a){M.css("cursor",a)},enableHandles:E,enableOnly:function(){h=!0},showHandles:C,disableHandles:F,animMode:G,setBgOpacity:z,done:L}}(),bc=function(){function f(b){M.css({zIndex:450}),b?a(document).bind("touchmove.jcrop"
,k).bind("touchend.jcrop",l):e&&a(document).bind("mousemove.jcrop",h).bind("mouseup.jcrop",i)}function g(){M.css({zIndex:290}),a(document).unbind(".jcrop")}function h(a){return b(m(a)),!1}function i(a){return a.preventDefault(),a.stopPropagation(),W&&(W=!1,c(m(a)),bb.isAwake()&&d.onSelect.call(bs,u(_.getFixed())),g(),b=function(){},c=function(){}),!1}function j(a,d,e){return W=!0,b=a,c=d,f(e),!1}function k(a){return b(m(Z.cfilter(a))),!1}function l(a){return i(Z.cfilter(a))}function n(a){M.css("cursor",a)}var b=function(){},c=function(){},e=d.trackDocument;return e||M.mousemove(h).mouseup(i).mouseout(i),D.before(M),{activateHandlers:j,setCursor:n}}(),bd=function(){function e(){d.keySupport&&(b.show(),b.focus())}function f(a){b.hide()}function g(a,b,c){d.allowMove&&(_.moveOffset([b,c]),bb.updateVisible(!0)),a.preventDefault(),a.stopPropagation()}function i(a){if(a.ctrlKey||a.metaKey)return!0;Y=a.shiftKey?!0:!1;var b=Y?10:1;switch(a.keyCode){case 37:g(a,-b,0);break;case 39:g(a,b,0);break;case 38:g(a,0,-b);break;
case 40:g(a,0,b);break;case 27:d.allowSelect&&bb.release();break;case 9:return!0}return!1}var b=a('<input type="radio" />').css({position:"fixed",left:"-120px",width:"12px"}).addClass("jcrop-keymgr"),c=a("<div />").css({position:"absolute",overflow:"hidden"}).append(b);return d.keySupport&&(b.keydown(i).blur(f),h||!d.fixedSupport?(b.css({position:"absolute",left:"-20px"}),c.append(b).insertBefore(D)):b.insertBefore(D)),{watchKeys:e}}();Z.support&&M.bind("touchstart.jcrop",Z.newSelection),J.hide(),br(!0);var bs={setImage:bp,animateTo:bf,setSelect:bg,setOptions:bk,tellSelect:bi,tellScaled:bj,setClass:be,disable:bl,enable:bm,cancel:bn,release:bb.release,destroy:bo,focus:bd.watchKeys,getBounds:function(){return[E*T,F*U]},getWidgetSize:function(){return[E,F]},getScaleFactor:function(){return[T,U]},getOptions:function(){return d},ui:{holder:G,selection:K}};return g&&G.bind("selectstart",function(){return!1}),A.data("Jcrop",bs),bs},a.fn.Jcrop=function(b,c){var d;return this.each(function(){if(a(this).data("Jcrop")){if(
b==="api")return a(this).data("Jcrop");a(this).data("Jcrop").setOptions(b)}else this.tagName=="IMG"?a.Jcrop.Loader(this,function(){a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d)}):(a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d))}),this},a.Jcrop.Loader=function(b,c,d){function g(){f.complete?(e.unbind(".jcloader"),a.isFunction(c)&&c.call(f)):window.setTimeout(g,50)}var e=a(b),f=e[0];e.bind("load.jcloader",g).bind("error.jcloader",function(b){e.unbind(".jcloader"),a.isFunction(d)&&d.call(f)}),f.complete&&a.isFunction(c)&&(e.unbind(".jcloader"),c.call(f))},a.Jcrop.defaults={allowSelect:!0,allowMove:!0,allowResize:!0,trackDocument:!0,baseClass:"jcrop",addClass:null,bgColor:"black",bgOpacity:.6,bgFade:!1,borderOpacity:.4,handleOpacity:.5,handleSize:null,aspectRatio:0,keySupport:!0,createHandles:["n","s","e","w","nw","ne","se","sw"],createDragbars:["n","s","e","w"],createBorders:["n","s","e","w"],drawBorders:!0,dragEdges
:!0,fixedSupport:!0,touchSupport:null,shade:null,boxWidth:0,boxHeight:0,boundary:2,fadeTime:400,animationDelay:20,swingSpeed:3,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){},onDblClick:function(){},onRelease:function(){}}})(jQuery);

View File

@ -0,0 +1,661 @@
/*!
* jQuery Color Animations v2.0pre
* http://jquery.org/
*
* Copyright 2011 John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/
(function( jQuery, undefined ){
var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color outlineColor".split(" "),
// plusequals test for += 100 -= 100
rplusequals = /^([\-+])=\s*(\d+\.?\d*)/,
// a set of RE's that can match strings and generate color tuples.
stringParsers = [{
re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
parse: function( execResult ) {
return [
execResult[ 1 ],
execResult[ 2 ],
execResult[ 3 ],
execResult[ 4 ]
];
}
}, {
re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
parse: function( execResult ) {
return [
2.55 * execResult[1],
2.55 * execResult[2],
2.55 * execResult[3],
execResult[ 4 ]
];
}
}, {
re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
parse: function( execResult ) {
return [
parseInt( execResult[ 1 ], 16 ),
parseInt( execResult[ 2 ], 16 ),
parseInt( execResult[ 3 ], 16 )
];
}
}, {
re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,
parse: function( execResult ) {
return [
parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ),
parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ),
parseInt( execResult[ 3 ] + execResult[ 3 ], 16 )
];
}
}, {
re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
space: "hsla",
parse: function( execResult ) {
return [
execResult[1],
execResult[2] / 100,
execResult[3] / 100,
execResult[4]
];
}
}],
// jQuery.Color( )
color = jQuery.Color = function( color, green, blue, alpha ) {
return new jQuery.Color.fn.parse( color, green, blue, alpha );
},
spaces = {
rgba: {
cache: "_rgba",
props: {
red: {
idx: 0,
type: "byte",
empty: true
},
green: {
idx: 1,
type: "byte",
empty: true
},
blue: {
idx: 2,
type: "byte",
empty: true
},
alpha: {
idx: 3,
type: "percent",
def: 1
}
}
},
hsla: {
cache: "_hsla",
props: {
hue: {
idx: 0,
type: "degrees",
empty: true
},
saturation: {
idx: 1,
type: "percent",
empty: true
},
lightness: {
idx: 2,
type: "percent",
empty: true
}
}
}
},
propTypes = {
"byte": {
floor: true,
min: 0,
max: 255
},
"percent": {
min: 0,
max: 1
},
"degrees": {
mod: 360,
floor: true
}
},
rgbaspace = spaces.rgba.props,
support = color.support = {},
// colors = jQuery.Color.names
colors,
// local aliases of functions called often
each = jQuery.each;
spaces.hsla.props.alpha = rgbaspace.alpha;
function clamp( value, prop, alwaysAllowEmpty ) {
var type = propTypes[ prop.type ] || {},
allowEmpty = prop.empty || alwaysAllowEmpty;
if ( allowEmpty && value == null ) {
return null;
}
if ( prop.def && value == null ) {
return prop.def;
}
if ( type.floor ) {
value = ~~value;
} else {
value = parseFloat( value );
}
if ( value == null || isNaN( value ) ) {
return prop.def;
}
if ( type.mod ) {
value = value % type.mod;
// -10 -> 350
return value < 0 ? type.mod + value : value;
}
// for now all property types without mod have min and max
return type.min > value ? type.min : type.max < value ? type.max : value;
}
function stringParse( string ) {
var inst = color(),
rgba = inst._rgba = [];
string = string.toLowerCase();
each( stringParsers, function( i, parser ) {
var match = parser.re.exec( string ),
values = match && parser.parse( match ),
parsed,
spaceName = parser.space || "rgba",
cache = spaces[ spaceName ].cache;
if ( values ) {
parsed = inst[ spaceName ]( values );
// if this was an rgba parse the assignment might happen twice
// oh well....
inst[ cache ] = parsed[ cache ];
rgba = inst._rgba = parsed._rgba;
// exit each( stringParsers ) here because we matched
return false;
}
});
// Found a stringParser that handled it
if ( rgba.length !== 0 ) {
// if this came from a parsed string, force "transparent" when alpha is 0
// chrome, (and maybe others) return "transparent" as rgba(0,0,0,0)
if ( Math.max.apply( Math, rgba ) === 0 ) {
jQuery.extend( rgba, colors.transparent );
}
return inst;
}
// named colors / default - filter back through parse function
if ( string = colors[ string ] ) {
return string;
}
}
color.fn = color.prototype = {
constructor: color,
parse: function( red, green, blue, alpha ) {
if ( red === undefined ) {
this._rgba = [ null, null, null, null ];
return this;
}
if ( red instanceof jQuery || red.nodeType ) {
red = red instanceof jQuery ? red.css( green ) : jQuery( red ).css( green );
green = undefined;
}
var inst = this,
type = jQuery.type( red ),
rgba = this._rgba = [],
source;
// more than 1 argument specified - assume ( red, green, blue, alpha )
if ( green !== undefined ) {
red = [ red, green, blue, alpha ];
type = "array";
}
if ( type === "string" ) {
return this.parse( stringParse( red ) || colors._default );
}
if ( type === "array" ) {
each( rgbaspace, function( key, prop ) {
rgba[ prop.idx ] = clamp( red[ prop.idx ], prop );
});
return this;
}
if ( type === "object" ) {
if ( red instanceof color ) {
each( spaces, function( spaceName, space ) {
if ( red[ space.cache ] ) {
inst[ space.cache ] = red[ space.cache ].slice();
}
});
} else {
each( spaces, function( spaceName, space ) {
each( space.props, function( key, prop ) {
var cache = space.cache;
// if the cache doesn't exist, and we know how to convert
if ( !inst[ cache ] && space.to ) {
// if the value was null, we don't need to copy it
// if the key was alpha, we don't need to copy it either
if ( red[ key ] == null || key === "alpha") {
return;
}
inst[ cache ] = space.to( inst._rgba );
}
// this is the only case where we allow nulls for ALL properties.
// call clamp with alwaysAllowEmpty
inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true );
});
});
}
return this;
}
},
is: function( compare ) {
var is = color( compare ),
same = true,
myself = this;
each( spaces, function( _, space ) {
var isCache = is[ space.cache ],
localCache;
if (isCache) {
localCache = myself[ space.cache ] || space.to && space.to( myself._rgba ) || [];
each( space.props, function( _, prop ) {
if ( isCache[ prop.idx ] != null ) {
same = ( isCache[ prop.idx ] === localCache[ prop.idx ] );
return same;
}
});
}
return same;
});
return same;
},
_space: function() {
var used = [],
inst = this;
each( spaces, function( spaceName, space ) {
if ( inst[ space.cache ] ) {
used.push( spaceName );
}
});
return used.pop();
},
transition: function( other, distance ) {
var end = color( other ),
spaceName = end._space(),
space = spaces[ spaceName ],
start = this[ space.cache ] || space.to( this._rgba ),
result = start.slice();
end = end[ space.cache ];
each( space.props, function( key, prop ) {
var index = prop.idx,
startValue = start[ index ],
endValue = end[ index ],
type = propTypes[ prop.type ] || {};
// if null, don't override start value
if ( endValue === null ) {
return;
}
// if null - use end
if ( startValue === null ) {
result[ index ] = endValue;
} else {
if ( type.mod ) {
if ( endValue - startValue > type.mod / 2 ) {
startValue += type.mod;
} else if ( startValue - endValue > type.mod / 2 ) {
startValue -= type.mod;
}
}
result[ prop.idx ] = clamp( ( endValue - startValue ) * distance + startValue, prop );
}
});
return this[ spaceName ]( result );
},
blend: function( opaque ) {
// if we are already opaque - return ourself
if ( this._rgba[ 3 ] === 1 ) {
return this;
}
var rgb = this._rgba.slice(),
a = rgb.pop(),
blend = color( opaque )._rgba;
return color( jQuery.map( rgb, function( v, i ) {
return ( 1 - a ) * blend[ i ] + a * v;
}));
},
toRgbaString: function() {
var prefix = "rgba(",
rgba = jQuery.map( this._rgba, function( v, i ) {
return v == null ? ( i > 2 ? 1 : 0 ) : v;
});
if ( rgba[ 3 ] === 1 ) {
rgba.pop();
prefix = "rgb(";
}
return prefix + rgba.join(",") + ")";
},
toHslaString: function() {
var prefix = "hsla(",
hsla = jQuery.map( this.hsla(), function( v, i ) {
if ( v == null ) {
v = i > 2 ? 1 : 0;
}
// catch 1 and 2
if ( i && i < 3 ) {
v = Math.round( v * 100 ) + "%";
}
return v;
});
if ( hsla[ 3 ] === 1 ) {
hsla.pop();
prefix = "hsl(";
}
return prefix + hsla.join(",") + ")";
},
toHexString: function( includeAlpha ) {
var rgba = this._rgba.slice(),
alpha = rgba.pop();
if ( includeAlpha ) {
rgba.push( ~~( alpha * 255 ) );
}
return "#" + jQuery.map( rgba, function( v, i ) {
// default to 0 when nulls exist
v = ( v || 0 ).toString( 16 );
return v.length === 1 ? "0" + v : v;
}).join("");
},
toString: function() {
return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString();
}
};
color.fn.parse.prototype = color.fn;
// hsla conversions adapted from:
// http://www.google.com/codesearch/p#OAMlx_jo-ck/src/third_party/WebKit/Source/WebCore/inspector/front-end/Color.js&d=7&l=193
function hue2rgb( p, q, h ) {
h = ( h + 1 ) % 1;
if ( h * 6 < 1 ) {
return p + (q - p) * 6 * h;
}
if ( h * 2 < 1) {
return q;
}
if ( h * 3 < 2 ) {
return p + (q - p) * ((2/3) - h) * 6;
}
return p;
}
spaces.hsla.to = function ( rgba ) {
if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) {
return [ null, null, null, rgba[ 3 ] ];
}
var r = rgba[ 0 ] / 255,
g = rgba[ 1 ] / 255,
b = rgba[ 2 ] / 255,
a = rgba[ 3 ],
max = Math.max( r, g, b ),
min = Math.min( r, g, b ),
diff = max - min,
add = max + min,
l = add * 0.5,
h, s;
if ( min === max ) {
h = 0;
} else if ( r === max ) {
h = ( 60 * ( g - b ) / diff ) + 360;
} else if ( g === max ) {
h = ( 60 * ( b - r ) / diff ) + 120;
} else {
h = ( 60 * ( r - g ) / diff ) + 240;
}
if ( l === 0 || l === 1 ) {
s = l;
} else if ( l <= 0.5 ) {
s = diff / add;
} else {
s = diff / ( 2 - add );
}
return [ Math.round(h) % 360, s, l, a == null ? 1 : a ];
};
spaces.hsla.from = function ( hsla ) {
if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) {
return [ null, null, null, hsla[ 3 ] ];
}
var h = hsla[ 0 ] / 360,
s = hsla[ 1 ],
l = hsla[ 2 ],
a = hsla[ 3 ],
q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s,
p = 2 * l - q,
r, g, b;
return [
Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ),
Math.round( hue2rgb( p, q, h ) * 255 ),
Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ),
a
];
};
each( spaces, function( spaceName, space ) {
var props = space.props,
cache = space.cache,
to = space.to,
from = space.from;
// makes rgba() and hsla()
color.fn[ spaceName ] = function( value ) {
// generate a cache for this space if it doesn't exist
if ( to && !this[ cache ] ) {
this[ cache ] = to( this._rgba );
}
if ( value === undefined ) {
return this[ cache ].slice();
}
var type = jQuery.type( value ),
arr = ( type === "array" || type === "object" ) ? value : arguments,
local = this[ cache ].slice(),
ret;
each( props, function( key, prop ) {
var val = arr[ type === "object" ? key : prop.idx ];
if ( val == null ) {
val = local[ prop.idx ];
}
local[ prop.idx ] = clamp( val, prop );
});
if ( from ) {
ret = color( from( local ) );
ret[ cache ] = local;
return ret;
} else {
return color( local );
}
};
// makes red() green() blue() alpha() hue() saturation() lightness()
each( props, function( key, prop ) {
// alpha is included in more than one space
if ( color.fn[ key ] ) {
return;
}
color.fn[ key ] = function( value ) {
var vtype = jQuery.type( value ),
fn = ( key === 'alpha' ? ( this._hsla ? 'hsla' : 'rgba' ) : spaceName ),
local = this[ fn ](),
cur = local[ prop.idx ],
match;
if ( vtype === "undefined" ) {
return cur;
}
if ( vtype === "function" ) {
value = value.call( this, cur );
vtype = jQuery.type( value );
}
if ( value == null && prop.empty ) {
return this;
}
if ( vtype === "string" ) {
match = rplusequals.exec( value );
if ( match ) {
value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 );
}
}
local[ prop.idx ] = value;
return this[ fn ]( local );
};
});
});
// add .fx.step functions
each( stepHooks, function( i, hook ) {
jQuery.cssHooks[ hook ] = {
set: function( elem, value ) {
var parsed, backgroundColor, curElem;
if ( jQuery.type( value ) !== 'string' || ( parsed = stringParse( value ) ) )
{
value = color( parsed || value );
if ( !support.rgba && value._rgba[ 3 ] !== 1 ) {
curElem = hook === "backgroundColor" ? elem.parentNode : elem;
do {
backgroundColor = jQuery.curCSS( curElem, "backgroundColor" );
} while (
( backgroundColor === "" || backgroundColor === "transparent" ) &&
( curElem = curElem.parentNode ) &&
curElem.style
);
value = value.blend( backgroundColor && backgroundColor !== "transparent" ?
backgroundColor :
"_default" );
}
value = value.toRgbaString();
}
elem.style[ hook ] = value;
}
};
jQuery.fx.step[ hook ] = function( fx ) {
if ( !fx.colorInit ) {
fx.start = color( fx.elem, hook );
fx.end = color( fx.end );
fx.colorInit = true;
}
jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) );
};
});
// detect rgba support
jQuery(function() {
var div = document.createElement( "div" ),
div_style = div.style;
div_style.cssText = "background-color:rgba(1,1,1,.5)";
support.rgba = div_style.backgroundColor.indexOf( "rgba" ) > -1;
});
// Some named colors to work with
// From Interface by Stefan Petre
// http://interface.eyecon.ro/
colors = jQuery.Color.names = {
aqua: "#00ffff",
azure: "#f0ffff",
beige: "#f5f5dc",
black: "#000000",
blue: "#0000ff",
brown: "#a52a2a",
cyan: "#00ffff",
darkblue: "#00008b",
darkcyan: "#008b8b",
darkgrey: "#a9a9a9",
darkgreen: "#006400",
darkkhaki: "#bdb76b",
darkmagenta: "#8b008b",
darkolivegreen: "#556b2f",
darkorange: "#ff8c00",
darkorchid: "#9932cc",
darkred: "#8b0000",
darksalmon: "#e9967a",
darkviolet: "#9400d3",
fuchsia: "#ff00ff",
gold: "#ffd700",
green: "#008000",
indigo: "#4b0082",
khaki: "#f0e68c",
lightblue: "#add8e6",
lightcyan: "#e0ffff",
lightgreen: "#90ee90",
lightgrey: "#d3d3d3",
lightpink: "#ffb6c1",
lightyellow: "#ffffe0",
lime: "#00ff00",
magenta: "#ff00ff",
maroon: "#800000",
navy: "#000080",
olive: "#808000",
orange: "#ffa500",
pink: "#ffc0cb",
purple: "#800080",
violet: "#800080",
red: "#ff0000",
silver: "#c0c0c0",
white: "#ffffff",
yellow: "#ffff00",
transparent: [ null, null, null, 0 ],
_default: "#ffffff"
};
})( jQuery );

View File

@ -0,0 +1,33 @@
{% extends "layout.html" %}
{% block title %}{{ album }}{% endblock %}
{% block content %}
<h1>Album: {{ album }}</h1>
<ul class="photo-grid">
{% for photo in photos %}
<li class="portrait">
<div class="dummy"></div>
<div class="photo-grid-item">
<a href="{{ url_for('photo.view_photo', key=photo['key']) }}">
<img src="{{ app['photo_url'] }}/{{ photo['data']['thumb'] }}" width="100%" height="100%">
<span class="name">{{ photo["data"]["caption"] }}</span>
</a>
</div>
</li>
{% endfor %}
</ul>
<div class="clear"></div>
{% if session["login"] %}
<h1>Administrative Options</h1>
<ul>
<li><a href="{{ url_for('photo.upload') }}">Upload a Photo</a></li>
{% if photos|length > 0 %}<li><a href="{{ url_for('photo.arrange_photos', album=album) }}">Rearrange Photos</a></li>{% endif %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "layout.html" %}
{% block title %}Photo Albums{% endblock %}
{% block content %}
<h1>Photo Albums</h1>
{% if albums|length == 0 %}
<em>There are no photo albums yet.</em>
{% else %}
<ul class="photo-grid">
{% for album in albums %}
<li class="portrait">
<div class="dummy"></div>
<div class="photo-grid-item">
<a href="{{ url_for('photo.album_index', name=album['name']) }}">
<img src="{{ app['photo_url'] }}/{{ album['cover'] }}" width="100%" height="100%">
<span class="name">{{ album["name"] }}</span>
</a>
</div>
</li>
{% endfor %}
</ul>
<div class="clear"></div>
{% endif %}
{% if session["login"] %}
<h1>Administrative Options</h1>
<ul>
<li><a href="{{ url_for('photo.upload') }}">Upload a Photo</a></li>
{% if albums|length > 0 %}<li><a href="{{ url_for('photo.arrange_albums') }}">Rearrange Albums</a></li>{% endif %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "layout.html" %}
{% block title %}Arrange Albums{% endblock %}
{% block content %}
<h1>Arrange Albums</h1>
Drag and drop your albums in the positions you want them in.<p>
<form name="arrange" id="arrange-form" action="{{ url_for('photo.arrange_albums') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="order" id="order" value="">
<button type="submit">Save Changes</button>
</form>
<p>
<ul id="arrange-photos" class="photo-grid">
{% for album in albums %}
<li data-name="{{ album['name'] }}" class="portrait">
<div class="dummy"></div>
<div class="photo-grid-item">
<a href="{{ url_for('photo.album_index', name=album['name']) }}">
<img src="{{ app['photo_url'] }}/{{ album['cover'] }}" width="100%" height="100%">
<span class="name">{{ album["name"] }}</span>
</a>
</div>
</li>
{% endfor %}
</ul>
<div class="clear"></div>
{% endblock %}
{% block scripts %}
<link rel="stylesheet" type="text/css" media="all" href="/css/ui-lightness/jquery-ui-1.10.4.custom.css">
<script src="/js/jquery-ui-1.10.4.custom.js"></script>
<script>
$(document).ready(function() {
var $photos = $("#arrange-photos"),
$form = $("#arrange-form"),
$order = $("#order");
var doArrangePhotos = function() {
var order = [];
// Get the list of elements.
var list = $photos.children().each(function() {
var name = $(this).data("name");
order.push(name);
});
$order.val(order.join(";"));
return true;
};
$form.submit(doArrangePhotos);
$photos.sortable();
$photos.disableSelection();
});
</script>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "layout.html" %}
{% block title %}Arrange Photos{% endblock %}
{% block content %}
<h1>Arrange Photos</h1>
Drag and drop your photos in the positions you want them in.<p>
<form name="arrange" id="arrange-form" action="{{ url_for('photo.arrange_photos', album=album) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="order" id="order" value="">
<button type="submit">Save Changes</button>
</form>
<p>
<ul id="arrange-photos" class="photo-grid">
{% for photo in photos %}
<li data-name="{{ photo['key'] }}" class="portrait">
<div class="dummy"></div>
<div class="photo-grid-item">
<img src="{{ app['photo_url'] }}/{{ photo['data']['thumb'] }}" width="100%" height="100%">
<span class="name">{{ photo['data']['caption'] }}</span>
</div>
</li>
{% endfor %}
</ul>
<div class="clear"></div>
{% endblock %}
{% block scripts %}
<link rel="stylesheet" type="text/css" media="all" href="/css/ui-lightness/jquery-ui-1.10.4.custom.css">
<script src="/js/jquery-ui-1.10.4.custom.js"></script>
<script>
$(document).ready(function() {
var $photos = $("#arrange-photos"),
$form = $("#arrange-form"),
$order = $("#order");
var doArrangePhotos = function() {
var order = [];
// Get the list of elements.
var list = $photos.children().each(function() {
var name = $(this).data("name");
order.push(name);
});
$order.val(order.join(";"));
return true;
};
$form.submit(doArrangePhotos);
$photos.sortable();
$photos.disableSelection();
});
</script>
{% endblock %}

View File

@ -0,0 +1,107 @@
{% extends "layout.html" %}
{% block title %}Crop Photo{% endblock %}
{% block content %}
<h1>Crop Photo</h1>
All versions of your photo except the largest one are cropped into a square
shape. You can use this page to modify the region of the photo you want to
crop.<p>
<table border="0" cellspacing="4" cellpadding="2">
<tr>
<td align="center" valign="middle">
<img src="{{ app['photo_url'] }}/{{ preview }}" id="cropbox">
</td>
<td align="center" valign="top">
<strong>Preview:</strong><br>
<div style="width: 100px; height: 100px; overflow: hidden">
<img src="{{ app['photo_url'] }}/{{ preview }}" id="preview" style="max-width: none">
</div>
<p>
<form name="crop" action="{{ url_for('photo.crop', photo=photo) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="x" id="x" value="0">
<input type="hidden" name="y" id="y" value="0">
<input type="hidden" name="length" id="length" value="0">
<button type="submit">Crop Photo!</button>
</form>
</td>
</tr>
</table>
{% endblock %}
{% block scripts %}
<script src="/js/jquery.Jcrop.min.js"></script>
<link rel="stylesheet" type="text/css" href="/css/jquery.Jcrop.css">
<script>
$(document).ready(function() {
var $cropbox = $("#cropbox"),
$preview = $("#preview"),
$x = $("#x"),
$y = $("#y"),
$length = $("#length");
// Find the shortest side.
var len;
if ($cropbox.width() > $cropbox.height()) {
len = $cropbox.height();
}
else {
len = $cropbox.width();
}
// Jcrop handler.
var showPreview = function(coords) {
if (parseInt(coords.w) > 0) {
var rx = 100 / coords.w;
var ry = 100 / coords.h;
var ht = $cropbox.height();
var wt = $cropbox.width();
// Make the coords into percentages, so it works on mobile.
// Get the true dimensions of the image from PIL.
var trueW = {{ true_width }};
var trueH = {{ true_height }};
// The actual (possibly scaled) image shown on the page is hereby called
// the "display image"... turn our "display coords" into percentages
// across the image.
var percentX = coords.x / wt;
var percentY = coords.y / ht;
var percentLen = coords.w / wt;
// Now get our true coords by multiplying those percentages against the
// true dimensions of the image from PIL.
var trueX = trueW * percentX;
var trueY = trueH * percentY;
var trueLen = trueW * percentLen;
// Update the preview.
$preview.css({
width: Math.round(rx * wt) + "px",
height: Math.round(ry * ht) + "px",
marginLeft: "-" + Math.round(rx * coords.x) + "px",
marginTop: "-" + Math.round(ry * coords.y) + "px"
})
// Update the form.
$x.val(parseInt(trueX));
$y.val(parseInt(trueY));
$length.val(parseInt(trueLen));
}
}
$cropbox.Jcrop({
onChange: showPreview,
onSelect: showPreview,
aspectRatio: 1,
setSelect: [ 0, 0, len, len ],
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block title %}Delete Photo{% endblock %}
{% block content %}
<h1>Delete Photo</h1>
<img src="{{ app['photo_url'] }}/{{ photo['thumb'] }}" class="portrait"><p>
<form name="delete" action="{{ url_for('photo.delete', key=key) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
Are you <strong>sure</strong> you want to delete this photo?<p>
<button type="submit">Yes, Delete This Photo</button>
</form>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block title %}Edit Photo{% endblock %}
{% block content %}
<h1>Edit Photo</h1>
<img src="{{ app['photo_url'] }}/{{ photo['thumb'] }}" class="portrait"><p>
<form name="edit" action="{{ url_for('photo.edit', key=key) }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<strong>Photo Caption:</strong><br>
<input type="text" size="40" name="caption" value="{{ photo['caption'] }}"><p>
<button type="submit">Save Changes</button>
</form>
{% endblock %}

View File

@ -0,0 +1,89 @@
{% extends "layout.html" %}
{% block title %}Upload a Photo{% endblock %}
{% block content %}
<h1>Upload a Photo</h1>
You can upload a photo from your computer or by pasting in the URL to a photo
somewhere else on the Internet.
<form name="upload" action="{{ url_for('photo.upload') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<fieldset>
<legend>Where is your picture located?</legend>
<label>
<input type="radio" class="location" name="location" value="pc" checked> On my computer
</label>
<label>
<input type="radio" class="location" name="location" value="www"> On the web
</label><p>
<div id="pic-pc" class="location-div">
<strong>Upload a picture from my computer</strong><br>
<input type="file" size="30" name="file">
</div>
<div id="pic-www" class="location-div">
<strong>Upload a picture from the Internet</strong><br>
<input type="text" size="40" name="url" placeholder="http://" autocomplete="off">
</div>
<p>
Only jpeg, gif and png images are supported. There is no maximum file size
limit, but be reasonable.
</fieldset>
<fieldset>
<legend>Photo Options</legend>
<strong>Photo album:</strong><br>
<select id="album" name="album">
<optgroup label="Albums">
{% for album in album_list %}
<option value="{{ album }}"{% if album == selected %} selected{% endif %}>{{ album }}</option>
{% endfor %}
</optgroup>
<option value="">Create a new album</option>
</select><p>
<blockquote id="create-album">
<strong>New album:</strong><br>
<input type="text" size="20" id="new-album" name="new-album">
</blockquote>
<strong>Caption:</strong><br>
<input type="text" size="40" name="caption">
</fieldset>
<p>
<button type="submit">Upload Picture</button>
</form>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$("#pic-www").hide();
$("#create-album").hide();
$(".location").change(function() {
$(".location-div").hide();
$("#pic-" + $(this).val()).show();
});
$("#album").change(function() {
if ($(this).val() === "") {
$("#create-album").show();
}
else {
$("#new-album").val("");
$("#create-album").hide();
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,49 @@
{% extends "layout.html" %}
{% block title %}{{ photo["caption"] or "Photo" }}{% endblock %}
{% block content %}
{% macro nav_links() %}
<div class="right">
Photo {{ photo["position"] }} of {{ photo["siblings"] }}
{% if photo["siblings"] > 1 %}
&nbsp;&nbsp;&nbsp;
[
<a href="{{ url_for('photo.view_photo', key=photo['previous']) }}">&lt; Previous</a>
|
<a href="{{ url_for('photo.view_photo', key=photo['next']) }}">Next &gt;</a>
]
{% endif %}
</div>
{% endmacro %}
<h1>Photo</h1>
<a href="{{ url_for('photo.album_index', name=photo['album']) }}">&lt; Back to Album</a><p>
{{ nav_links() }}
<div class="center">
<a href="{{ url_for('photo.view_photo', key=photo['next']) }}">
<img src="{{ app['photo_url'] }}/{{ photo['large'] }}" class="portrait">
</a><br>
<strong>{{ photo["caption"] }}</strong>
</div>
<p>
Uploaded by {{ author["name"] }} on {{ photo["pretty_time"] }}.
{{ nav_links() }}
{% if session["login"] %}
<h1>Administrative Options</h1>
<ul>
<li><a href="{{ url_for('photo.upload') }}">Upload a Photo</a></li>
<li><a href="{{ url_for('photo.set_cover', album=photo['album'], key=photo['key']) }}">Set Album Cover</a></li>
<li><a href="{{ url_for('photo.set_profile', key=photo['key']) }}">Set as my Profile Picture</a></li>
<li><a href="{{ url_for('photo.edit', key=photo['key']) }}">Edit this photo</a></li>
<li><a href="{{ url_for('photo.delete', key=photo['key']) }}">Delete this photo</a></li>
</ul>
{% endif %}
{% endblock %}