checkpoint

master
voussoir 2016-10-09 20:50:13 -07:00
parent 6c5580c1bc
commit 0b85c309f8
19 changed files with 1196 additions and 488 deletions

View File

@ -1,22 +1,29 @@
import distutils.util
import flask
from flask import request
import functools
import json
import math
import mimetypes
import os
import random
import re
import requests
import sys
import time
import uuid
import warnings
import phototagger
sys.path.append('C:\\git\\else\\Bytestring'); import bytestring
site = flask.Flask(__name__)
site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180,
TEMPLATES_AUTO_RELOAD=True,
)
site.jinja_env.add_extension('jinja2.ext.do')
print(os.getcwd())
import phototagger
P = phototagger.PhotoDB()
FILE_READ_CHUNK = 2 ** 20
@ -32,17 +39,60 @@ ERROR_TAG_TOO_SHORT = 'Not enough valid chars'
ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself'
ERROR_NO_SUCH_TAG = 'Doesn\'t exist'
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
def give_session_token(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
# Inject new token so the function doesn't know the difference
token = request.cookies.get('etiquette_session', None)
if not token:
token = generate_session_token()
request.cookies = dict(request.cookies)
request.cookies['etiquette_session'] = token
ret = function(*args, **kwargs)
# Send the token back to the client
if not isinstance(ret, flask.Response):
ret = flask.Response(ret)
ret.set_cookie('etiquette_session', value=token, max_age=60)
return ret
return wrapped
def _helper_comma_split(s):
if s is None:
return s
s = s.replace(' ', ',')
s = [x.strip() for x in s.split(',')]
s = [x for x in s if x]
return s
def edit_params(original, modifications):
new_params = original.to_dict()
new_params.update(modifications)
if not new_params:
return ''
keep_params = {}
new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v]
new_params = '&'.join(new_params)
new_params = '?' + new_params
return new_params
def generate_session_token():
token = str(uuid.uuid4())
#print('MAKE SESSION', token)
return token
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response
def P_album(albumid):
try:
return P.get_album(albumid)
@ -80,6 +130,7 @@ def read_filebytes(filepath, range_min, range_max):
sent_amount = 0
with f:
while sent_amount < range_span:
print(sent_amount)
chunk = f.read(FILE_READ_CHUNK)
if len(chunk) == 0:
break
@ -91,6 +142,11 @@ def send_file(filepath):
'''
Range-enabled file sending.
'''
try:
file_size = os.path.getsize(filepath)
except FileNotFoundError:
flask.abort(404)
outgoing_headers = {}
mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None:
@ -98,202 +154,54 @@ def send_file(filepath):
mimetype += '; charset=utf-8'
outgoing_headers['Content-Type'] = mimetype
if 'range' not in request.headers:
response = flask.make_response(flask.send_file(filepath))
for (k, v) in outgoing_headers.items():
response.headers[k] = v
return response
if 'range' in request.headers:
desired_range = request.headers['range'].lower()
desired_range = desired_range.split('bytes=')[-1]
try:
file_size = os.path.getsize(filepath)
except FileNotFoundError:
flask.abort(404)
int_helper = lambda x: int(x) if x.isdigit() else None
if '-' in desired_range:
(desired_min, desired_max) = desired_range.split('-')
range_min = int_helper(desired_min)
range_max = int_helper(desired_max)
else:
range_min = int_helper(desired_range)
desired_range = request.headers['range'].lower()
desired_range = desired_range.split('bytes=')[-1]
if range_min is None:
range_min = 0
if range_max is None:
range_max = file_size
inthelper = lambda x: int(x) if x.isdigit() else None
if '-' in desired_range:
(desired_min, desired_max) = desired_range.split('-')
range_min = inthelper(desired_min)
range_max = inthelper(desired_max)
# because ranges are 0-indexed
range_max = min(range_max, file_size - 1)
range_min = max(range_min, 0)
range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
status = 206
else:
range_min = inthelper(desired_range)
if range_min is None:
range_max = file_size - 1
range_min = 0
if range_max is None:
range_max = file_size
status = 200
# because ranges are 0-indexed
range_max = min(range_max, file_size - 1)
range_min = max(range_min, 0)
range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
outgoing_headers['Accept-Ranges'] = 'bytes'
outgoing_headers['Content-Length'] = (range_max - range_min) + 1
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max)
if request.method == 'HEAD':
outgoing_data = bytes()
else:
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max)
response = flask.Response(
outgoing_data,
status=206,
status=status,
headers=outgoing_headers,
)
return response
@site.route('/')
def root():
motd = random.choice(MOTD_STRINGS)
return flask.render_template('root.html', motd=motd)
@site.route('/album/<albumid>')
def get_album(albumid):
album = P_album(albumid)
response = flask.render_template(
'album.html',
album=album,
child_albums=album.children(),
photos=album.photos()
)
return response
@site.route('/file/<photoid>')
def get_file(photoid):
requested_photoid = photoid
photoid = photoid.split('.')[0]
photo = P.get_photo(photoid)
do_download = request.args.get('download', False)
do_download = truthystring(do_download)
use_original_filename = request.args.get('original_filename', False)
use_original_filename = truthystring(use_original_filename)
if do_download:
if use_original_filename:
download_as = photo.basename
else:
download_as = photo.id + '.' + photo.extension
# Sorry, but otherwise the attachment filename gets terminated
#download_as = download_as.replace(';', '-')
download_as = download_as.replace('"', '\\"')
response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as
return response
else:
return send_file(photo.real_filepath)
@site.route('/albums')
def get_albums():
albums = P.get_albums()
albums = [a for a in albums if a.parent() is None]
return flask.render_template('albums.html', albums=albums)
@site.route('/photo/<photoid>', methods=['GET'])
def get_photo(photoid):
photo = P_photo(photoid)
tags = photo.tags()
tags.sort(key=lambda x: x.qualified_name())
return flask.render_template('photo.html', photo=photo, tags=tags)
@site.route('/tags')
@site.route('/tags/<specific_tag>')
def get_tags(specific_tag=None):
try:
tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag)
except phototagger.NoSuchTag:
flask.abort(404, 'That tag doesnt exist')
tags = tags.split('\n')
tags = [t for t in tags if t != '']
tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags]
return flask.render_template('tags.html', tags=tags)
@site.route('/thumbnail/<photoid>')
def get_thumbnail(photoid):
photoid = photoid.split('.')[0]
photo = P_photo(photoid)
if photo.thumbnail:
path = photo.thumbnail
else:
flask.abort(404, 'That file doesnt have a thumbnail')
return send_file(path)
@site.route('/photo/<photoid>', methods=['POST'])
def edit_photo(photoid):
print(request.form)
response = {}
photo = P_photo(photoid)
if 'add_tag' in request.form:
action = 'add_tag'
method = photo.add_tag
elif 'remove_tag' in request.form:
action = 'remove_tag'
method = photo.remove_tag
else:
flask.abort(400, 'Invalid action')
tag = request.form[action].strip()
if tag == '':
flask.abort(400, 'No tag supplied')
try:
tag = P.get_tag(tag)
except phototagger.NoSuchTag:
return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404)
method(tag)
response['action'] = action
response['tagid'] = tag.id
response['tagname'] = tag.name
return json.dumps(response)
@site.route('/tags', methods=['POST'])
def edit_tags():
print(request.form)
status = 200
if 'create_tag' in request.form:
action = 'create_tag'
method = create_tag
elif 'delete_tag_synonym' in request.form:
action = 'delete_tag_synonym'
method = delete_synonym
elif 'delete_tag' in request.form:
action = 'delete_tag'
method = delete_tag
else:
response = {'error': ERROR_INVALID_ACTION}
if status == 200:
status = 400
tag = request.form[action].strip()
if tag == '':
response = {'error': ERROR_NO_TAG_GIVEN}
try:
response = method(tag)
except phototagger.TagTooShort:
response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag}
except phototagger.CantSynonymSelf:
response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag}
except phototagger.NoSuchTag as e:
response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag}
except ValueError as e:
response = {'error': e.args[0], 'tagname': tag}
else:
status = 200
response = json.dumps(response)
response = flask.Response(response, status=status)
return response
def create_tag(easybake_string):
notes = P.easybake(easybake_string)
notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes]
@ -320,24 +228,170 @@ def delete_synonym(synonym):
master_tag.remove_synonym(synonym)
return {'action':'delete_synonym', 'synonym': synonym}
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
@site.route('/search')
def search():
def jsonify_album(album, minimal=False):
j = {
'id': album.id,
'description': album.description,
'title': album.title,
}
if minimal is False:
j['photos'] = [jsonify_photo(photo) for photo in album.photos()]
j['parent'] = album.parent()
j['sub_albums'] = [child.id for child in album.children()]
return j
def seconds_to_hms(seconds):
seconds = math.ceil(seconds)
(minutes, seconds) = divmod(seconds, 60)
(hours, minutes) = divmod(minutes, 60)
parts = []
if hours: parts.append(hours)
if minutes: parts.append(minutes)
parts.append(seconds)
hms = ':'.join('%02d' % part for part in parts)
return hms
def jsonify_photo(photo):
tags = photo.tags()
tags.sort(key=lambda x: x.name)
j = {
'id': photo.id,
'extension': photo.extension,
'width': photo.width,
'height': photo.height,
'ratio': photo.ratio,
'area': photo.area,
'bytes': photo.bytes,
'duration': seconds_to_hms(photo.duration) if photo.duration else None,
'duration_int': photo.duration,
'bytestring': photo.bytestring(),
'has_thumbnail': bool(photo.thumbnail),
'created': photo.created,
'filename': photo.basename,
'mimetype': photo.mimetype(),
'albums': [jsonify_album(album, minimal=True) for album in photo.albums()],
'tags': [jsonify_tag(tag) for tag in tags],
}
return j
def jsonify_tag(tag):
j = {
'id': tag.id,
'name': tag.name,
'qualified_name': tag.qualified_name(),
}
return j
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
@site.route('/')
@give_session_token
def root():
motd = random.choice(MOTD_STRINGS)
return flask.render_template('root.html', motd=motd)
@site.route('/favicon.ico')
@site.route('/favicon.png')
def favicon():
filename = os.path.join('static', 'favicon.png')
return flask.send_file(filename)
def get_album_core(albumid):
album = P_album(albumid)
album = jsonify_album(album)
return album
@site.route('/album/<albumid>')
@give_session_token
def get_album_html(albumid):
album = get_album_core(albumid)
response = flask.render_template(
'album.html',
album=album,
child_albums=album['sub_albums'],
photos=album['photos'],
)
return response
@site.route('/album/<albumid>')
@give_session_token
def get_album_json(albumid):
album = get_album_core(albumid)
return make_json_response(album)
@site.route('/albums')
@give_session_token
def get_albums():
albums = P.get_albums()
albums = [a for a in albums if a.parent() is None]
return flask.render_template('albums.html', albums=albums)
@site.route('/file/<photoid>')
def get_file(photoid):
requested_photoid = photoid
photoid = photoid.split('.')[0]
photo = P.get_photo(photoid)
do_download = request.args.get('download', False)
do_download = truthystring(do_download)
use_original_filename = request.args.get('original_filename', False)
use_original_filename = truthystring(use_original_filename)
if do_download:
if use_original_filename:
download_as = photo.basename
else:
download_as = photo.id + '.' + photo.extension
## Sorry, but otherwise the attachment filename gets terminated
#download_as = download_as.replace(';', '-')
download_as = download_as.replace('"', '\\"')
response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as
return response
else:
return send_file(photo.real_filepath)
def get_photo_core(photoid):
photo = P_photo(photoid)
photo = jsonify_photo(photo)
return photo
@site.route('/photo/<photoid>', methods=['GET'])
@give_session_token
def get_photo_html(photoid):
photo = get_photo_core(photoid)
photo['tags'].sort(key=lambda x: x['qualified_name'])
return flask.render_template('photo.html', photo=photo)
@site.route('/photo/<photoid>.json', methods=['GET'])
@give_session_token
def get_photo_json(photoid):
photo = get_photo_core(photoid)
photo = make_json_response(photo)
return photo
def get_search_core():
print(request.args)
def comma_split_helper(s):
s = s.replace(' ', ',')
s = [x.strip() for x in s.split(',')]
s = [x for x in s if x]
return s
# EXTENSION
extension_string = request.args.get('extension', '')
extension_not_string = request.args.get('extension_not', '')
mimetype_string = request.args.get('mimetype', '')
extension_string = request.args.get('extension', None)
extension_not_string = request.args.get('extension_not', None)
mimetype_string = request.args.get('mimetype', None)
extension_list = comma_split_helper(extension_string)
extension_not_list = comma_split_helper(extension_not_string)
mimetype_list = comma_split_helper(mimetype_string)
extension_list = _helper_comma_split(extension_string)
extension_not_list = _helper_comma_split(extension_not_string)
mimetype_list = _helper_comma_split(mimetype_string)
# LIMIT
limit = request.args.get('limit', '')
@ -387,7 +441,7 @@ def search():
height = request.args.get('height', None)
ratio = request.args.get('ratio', None)
bytes = request.args.get('bytes', None)
length = request.args.get('length', None)
duration = request.args.get('duration', None)
created = request.args.get('created', None)
# These are in a dictionary so I can pass them to the page template.
@ -397,7 +451,7 @@ def search():
'height': height,
'ratio': ratio,
'bytes': bytes,
'length': length,
'duration': duration,
'created': created,
'extension': extension_list,
@ -418,14 +472,16 @@ def search():
print(search_kwargs)
with warnings.catch_warnings(record=True) as catcher:
photos = list(P.search(**search_kwargs))
photos = [jsonify_photo(photo) for photo in photos]
warns = [str(warning.message) for warning in catcher]
print(warns)
# TAGS ON THIS PAGE
total_tags = set()
for photo in photos:
total_tags.update(photo.tags())
for tag in total_tags:
tag._cached_qualname = qualname_map[tag.name]
total_tags = sorted(total_tags, key=lambda x: x._cached_qualname)
for tag in photo['tags']:
total_tags.add(tag['qualified_name'])
total_tags = sorted(total_tags)
# PREV-NEXT PAGE URLS
offset = offset or 0
@ -443,23 +499,149 @@ def search():
search_kwargs['extension'] = extension_string
search_kwargs['extension_not'] = extension_not_string
search_kwargs['mimetype'] = mimetype_string
final_results = {
'next_page_url': next_page_url,
'prev_page_url': prev_page_url,
'photos': photos,
'total_tags': total_tags,
'warns': warns,
'search_kwargs': search_kwargs,
'qualname_map': qualname_map,
}
return final_results
@site.route('/search')
@give_session_token
def get_search_html():
search_results = get_search_core()
search_kwargs = search_results['search_kwargs']
qualname_map = search_results['qualname_map']
response = flask.render_template(
'search.html',
photos=photos,
search_kwargs=search_kwargs,
total_tags=total_tags,
prev_page_url=prev_page_url,
next_page_url=next_page_url,
next_page_url=search_results['next_page_url'],
prev_page_url=search_results['prev_page_url'],
photos=search_results['photos'],
qualname_map=json.dumps(qualname_map),
warns=warns,
search_kwargs=search_kwargs,
total_tags=search_results['total_tags'],
warns=search_results['warns'],
)
return response
@site.route('/search.json')
@give_session_token
def get_search_json():
search_results = get_search_core()
search_kwargs = search_results['search_kwargs']
qualname_map = search_results['qualname_map']
include_qualname_map = request.args.get('include_map', False)
include_qualname_map = truthystring(include_qualname_map)
if not include_qualname_map:
search_results.pop('qualname_map')
return make_json_response(j)
@site.route('/static/<filename>')
def get_resource(filename):
print(filename)
return flask.send_file('.\\static\\%s' % filename)
def get_static(filename):
filename = filename.replace('\\', os.sep)
filename = filename.replace('/', os.sep)
filename = os.path.join('static', filename)
return flask.send_file(filename)
@site.route('/tags')
@site.route('/tags/<specific_tag>')
@give_session_token
def get_tags(specific_tag=None):
try:
tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag)
except phototagger.NoSuchTag:
flask.abort(404, 'That tag doesnt exist')
tags = tags.split('\n')
tags = [t for t in tags if t != '']
tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags]
return flask.render_template('tags.html', tags=tags)
@site.route('/thumbnail/<photoid>')
def get_thumbnail(photoid):
photoid = photoid.split('.')[0]
photo = P_photo(photoid)
if photo.thumbnail:
path = photo.thumbnail
else:
flask.abort(404, 'That file doesnt have a thumbnail')
return send_file(path)
@site.route('/photo/<photoid>', methods=['POST'])
@give_session_token
def post_edit_photo(photoid):
print(request.form)
response = {}
photo = P_photo(photoid)
if 'add_tag' in request.form:
action = 'add_tag'
method = photo.add_tag
elif 'remove_tag' in request.form:
action = 'remove_tag'
method = photo.remove_tag
else:
flask.abort(400, 'Invalid action')
tag = request.form[action].strip()
if tag == '':
flask.abort(400, 'No tag supplied')
try:
tag = P.get_tag(tag)
except phototagger.NoSuchTag:
return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404)
method(tag)
response['action'] = action
response['tagid'] = tag.id
response['tagname'] = tag.name
return json.dumps(response)
@site.route('/tags', methods=['POST'])
@give_session_token
def post_edit_tags():
print(request.form)
status = 200
if 'create_tag' in request.form:
action = 'create_tag'
method = create_tag
elif 'delete_tag_synonym' in request.form:
action = 'delete_tag_synonym'
method = delete_synonym
elif 'delete_tag' in request.form:
action = 'delete_tag'
method = delete_tag
else:
response = {'error': ERROR_INVALID_ACTION}
if status == 200:
status = 400
tag = request.form[action].strip()
if tag == '':
response = {'error': ERROR_NO_TAG_GIVEN}
try:
response = method(tag)
except phototagger.TagTooShort:
response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag}
except phototagger.CantSynonymSelf:
response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag}
except phototagger.NoSuchTag as e:
response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag}
except ValueError as e:
response = {'error': e.args[0], 'tagname': tag}
else:
status = 200
response = json.dumps(response)
response = flask.Response(response, status=status)
return response
if __name__ == '__main__':
site.run(threaded=True)
site.run(threaded=True)

View File

@ -27,10 +27,13 @@ DEFAULT_THUMBDIR = '_site_thumbnails'
THUMBNAIL_WIDTH = 400
THUMBNAIL_HEIGHT = 400
ffmpeg = converter.Converter(
ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe',
ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe',
)
try:
ffmpeg = converter.Converter(
ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe',
ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe',
)
except converter.ffmpeg.FFMpegError:
ffmpeg = None
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
@ -40,8 +43,8 @@ ADDITIONAL_MIMETYPES = {
'srt': 'text',
'mkv': 'video',
}
WARNING_MINMAX_INVALID = 'Field {field}: "{value}" is not a valid request. Ignored.'
WARNING_MINMAX_OOO = 'Field {field}: minimum "{min}" maximum "{max}" are out of order. Ignored.'
WARNING_MINMAX_INVALID = 'Field "{field}": "{value}" is not a valid request. Ignored.'
WARNING_MINMAX_OOO = 'Field "{field}": minimum "{min}" maximum "{max}" are out of order. Ignored.'
WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.'
WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.'
WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.'
@ -66,7 +69,7 @@ SQL_PHOTO_COLUMNS = [
'height',
'ratio',
'area',
'length',
'duration',
'bytes',
'created',
'thumbnail',
@ -79,7 +82,6 @@ SQL_SYN_COLUMNS = [
'name',
'master',
]
SQL_ALBUMPHOTO_COLUMNS = [
'albumid',
'photoid',
@ -119,7 +121,7 @@ CREATE TABLE IF NOT EXISTS photos(
height INT,
ratio REAL,
area INT,
length INT,
duration INT,
bytes INT,
created INT,
thumbnail TEXT
@ -176,8 +178,30 @@ CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid);
CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid);
'''
def not_implemented(function):
'''
Decorator to remember what needs doing.
'''
warnings.warn('%s is not implemented' % function.__name__)
return function
def time_me(function):
'''
Decorator. After the function is run, print the elapsed time.
'''
@functools.wraps(function)
def timed_function(*args, **kwargs):
start = time.time()
result = function(*args, **kwargs)
end = time.time()
print('%s: %0.8f' % (function.__name__, end-start))
return result
return timed_function
def _helper_extension(ext):
'''
When searching, this function normalizes the list of permissible extensions.
'''
if isinstance(ext, str):
ext = [ext]
if ext is None:
@ -188,6 +212,10 @@ def _helper_extension(ext):
return ext
def _helper_minmax(key, value, minimums, maximums):
'''
When searching, this function dissects a hyphenated range string
and inserts the correct k:v pair into both minimums and maximums.
'''
if value is None:
return
if isinstance(value, (int, float)):
@ -207,6 +235,10 @@ def _helper_minmax(key, value, minimums, maximums):
maximums[key] = high
def _helper_orderby(orderby):
'''
When searching, this function ensures that the user has entered a valid orderby
query, and normalizes the query text.
'''
orderby = orderby.lower().strip()
if orderby == '':
return None
@ -227,7 +259,7 @@ def _helper_orderby(orderby):
'height',
'ratio',
'area',
'length',
'duration',
'bytes',
'created',
'random',
@ -244,6 +276,11 @@ def _helper_orderby(orderby):
return (column, sorter)
def _helper_setify(photodb, l, warn_bad_tags=False):
'''
When searching, this function converts the list of tag strings that the user
requested into Tag objects. If a tag doesn't exist we'll either raise an exception
or just issue a warning.
'''
if l is None:
return set()
@ -263,6 +300,18 @@ def _helper_setify(photodb, l, warn_bad_tags=False):
s.add(tag)
return s
def _helper_unitconvert(value):
'''
When parsing hyphenated ranges, this function is used to convert
strings like "1k" to 1024 and "1:00" to 60.
'''
if value is None:
return None
if ':' in value:
return hms_to_seconds(value)
else:
return bytestring.parsebytes(value)
def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
'''
Given a sequence, divide it into sequences of length `chunk_length`.
@ -286,25 +335,45 @@ def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
return chunks
def hms_to_seconds(hms):
'''
Convert hh:mm:ss string to an integer seconds.
'''
hms = hms.split(':')
seconds = 0
if len(hms) == 3:
seconds += int(hms[0])*3600
hms.pop(0)
if len(hms) == 2:
seconds += int(hms[0])*60
hms.pop(0)
if len(hms) == 1:
seconds += int(hms[0])
return seconds
def hyphen_range(s):
'''
Given a string like '1-3', return floats (1.0, 3.0) representing lower
Given a string like '1-3', return ints (1, 3) representing lower
and upper bounds.
Supports bytestring.parsebytes and hh:mm:ss format.
'''
s = s.strip()
s = s.replace(' ', '')
if not s:
return (None, None)
pattern = r'^(\d*\.?\d*)-(\d*\.?\d*)$'
try:
match = re.search(pattern, s)
low = match.group(1)
high = match.group(2)
except AttributeError:
low = float(s)
parts = s.split('-')
parts = [part.strip() or None for part in parts]
if len(parts) == 1:
low = parts[0]
high = None
elif len(parts) == 2:
(low, high) = parts
else:
low = float(low) if low.strip() else None
high = float(high) if high.strip() else None
raise ValueError('Too many hyphens')
low = _helper_unitconvert(low)
high = _helper_unitconvert(high)
if low is not None and high is not None and low > high:
raise OutOfOrder(s, low, high)
return low, high
@ -349,7 +418,7 @@ def is_xor(*args):
def normalize_filepath(filepath):
'''
Remove some bad characters
Remove some bad characters.
'''
filepath = filepath.replace('<', '')
filepath = filepath.replace('>', '')
@ -375,13 +444,6 @@ def normalize_tagname(tagname):
return tagname
def not_implemented(function):
'''
Decorator for keeping track of which functions still need to be filled out.
'''
warnings.warn('%s is not implemented' % function.__name__)
return function
def operate(operand_stack, operator_stack):
#print('before:', operand_stack, operator_stack)
operator = operator_stack.pop()
@ -488,7 +550,8 @@ def searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, f
return True
def select(sql, query, bindings=[]):
def select(sql, query, bindings=None):
bindings = bindings or []
cursor = sql.cursor()
cursor.execute(query, bindings)
while True:
@ -556,6 +619,7 @@ def tag_export_stdout(tags, depth=0):
if tag.parent() is None:
print()
@time_me
def tag_export_totally_flat(tags):
result = {}
for tag in tags:
@ -566,18 +630,6 @@ def tag_export_totally_flat(tags):
result[synonym] = children
return result
def time_me(function):
@functools.wraps(function)
def timed_function(*args, **kwargs):
start = time.time()
result = function(*args, **kwargs)
end = time.time()
print('%s: %0.8f' % (function.__name__, end-start))
return result
return timed_function
tag_export_totally_flat = time_me(tag_export_totally_flat)
####################################################################################################
####################################################################################################
@ -765,7 +817,7 @@ class PDBPhotoMixin:
data[SQL_PHOTO['height']] = None
data[SQL_PHOTO['area']] = None
data[SQL_PHOTO['ratio']] = None
data[SQL_PHOTO['length']] = None
data[SQL_PHOTO['duration']] = None
data[SQL_PHOTO['thumbnail']] = None
self.cur.execute('INSERT INTO photos VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data)
@ -782,7 +834,6 @@ class PDBPhotoMixin:
if commit:
log.debug('Commiting - new_photo')
self.sql.commit()
return photo
def search(
@ -792,7 +843,7 @@ class PDBPhotoMixin:
height=None,
ratio=None,
bytes=None,
length=None,
duration=None,
created=None,
extension=None,
@ -811,12 +862,12 @@ class PDBPhotoMixin:
):
'''
PHOTO PROPERTISE
area, width, height, ratio, bytes, length:
area, width, height, ratio, bytes, duration:
A hyphen_range string representing min and max. Or just a number for lower bound.
TAGS AND FILTERS
created:
A hyphen_range string respresenting min and max. Or jjust a number for lower bound.
A hyphen_range string respresenting min and max. Or just a number for lower bound.
extension:
A string or list of strings of acceptable file extensions.
@ -854,7 +905,7 @@ class PDBPhotoMixin:
Otherwise, a NoSuchTag exception would be raised.
limit:
The maximum number of *successful* results to yield.
The maximum number of *successful* results to yield.
offset:
How many *successful* results to skip before we start yielding.
@ -868,11 +919,12 @@ class PDBPhotoMixin:
maximums = {}
minimums = {}
_helper_minmax('area', area, minimums, maximums)
_helper_minmax('created', created, minimums, maximums)
_helper_minmax('width', width, minimums, maximums)
_helper_minmax('height', height, minimums, maximums)
_helper_minmax('ratio', ratio, minimums, maximums)
_helper_minmax('bytes', bytes, minimums, maximums)
_helper_minmax('length', length, minimums, maximums)
_helper_minmax('duration', duration, minimums, maximums)
orderby = orderby or []
extension = _helper_extension(extension)
@ -907,7 +959,14 @@ class PDBPhotoMixin:
# EVERY tag in the db is a key, and the value is a list of ALL ITS NESTED CHILDREN.
# This representation is memory inefficient, but it is faster than repeated
# database lookups
frozen_children = self.export_tags(tag_export_totally_flat)
is_must_may_forbid = bool(tag_musts or tag_mays or tag_forbids)
is_tagsearch = is_must_may_forbid or tag_expression
if is_tagsearch:
if self._cached_frozen_children:
frozen_children = self._cached_frozen_children
else:
frozen_children = self.export_tags(tag_export_totally_flat)
self._cached_frozen_children = frozen_children
photos_received = 0
for fetch in generator:
@ -932,7 +991,7 @@ class PDBPhotoMixin:
#print('Failed minimums')
continue
if (has_tags is not None) or tag_musts or tag_mays or tag_forbids or tag_expression:
if (has_tags is not None) or is_tagsearch:
photo_tags = photo.tags()
if has_tags is False and len(photo_tags) > 0:
@ -946,7 +1005,7 @@ class PDBPhotoMixin:
if tag_expression:
if not searchfilter_expression(photo_tags, tag_expression, frozen_children, warn_bad_tags):
continue
else:
elif is_must_may_forbid:
if not searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children):
continue
@ -956,7 +1015,7 @@ class PDBPhotoMixin:
if limit is not None and photos_received >= limit:
break
photos_received += 1
yield photo
@ -1031,7 +1090,9 @@ class PDBTagMixin:
pass
else:
raise TagExists(tagname)
tagid = self.generate_id('tags')
self._cached_frozen_children = None
self.cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname])
if commit:
log.debug('Commiting - new_tag')
@ -1047,7 +1108,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
albums:
Rows represent the inclusion of a photo in an album
photos:
photos:
Rows represent image files on the local disk.
Entries contain a unique ID, the image's filepath, and metadata
like dimensions and filesize.
@ -1088,9 +1149,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
for statement in statements:
self.cur.execute(statement)
self._cached_frozen_children = None
def __repr__(self):
return 'PhotoDB(databasename={dbname})'.format(dbname=repr(self.databasename))
def _uncache(self):
self._cached_frozen_children = None
def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True):
'''
Create an album, and add the directory's contents to it.
@ -1138,7 +1204,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
self.sql.commit()
return album
def digest_new_files(self, directory, exclude_directories=None, exclude_filenames=None, commit=True):
def digest_new_files(
self,
directory,
exclude_directories=None,
exclude_filenames=None,
recurse=True,
commit=True
):
'''
Walk the directory and add new files as Photos.
Does NOT create or modify any albums like `digest_directory` does.
@ -1161,6 +1234,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
directory,
exclude_directories=exclude_directories,
exclude_filenames=exclude_filenames,
recurse=recurse,
yield_style='flat',
)
for filepath in generator:
@ -1171,7 +1245,10 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
pass
else:
continue
photo = self.new_photo(filepath)
photo = self.new_photo(filepath, commit=False)
if commit:
log.debug('Committing - digest_new_files')
self.sql.commit()
def easybake(self, string):
@ -1229,7 +1306,6 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
output_notes.append(note)
else:
tag_parts = tag.split('.')
print('wtf')
tags = [create_or_get(t) for t in tag_parts]
for (higher, lower) in zip(tags, tags[1:]):
try:
@ -1271,7 +1347,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
else:
# Use database value
new_id_int = int(fetch[SQL_LASTID['last_id']]) + 1
new_id = str(new_id_int).rjust(self.id_length, '0')
if do_insert:
self.cur.execute('INSERT INTO id_numbers VALUES(?, ?)', [table, new_id])
@ -1367,6 +1443,7 @@ class GroupableMixin:
that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']])
raise GroupExists('%s already in group %s' % (member.name, that_group.name))
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id])
if commit:
log.debug('Commiting - add to group')
@ -1394,6 +1471,7 @@ class GroupableMixin:
If True, all children will be deleted.
Otherwise they'll just be raised up one level.
'''
self.photodb._cached_frozen_children = None
if delete_children:
for child in self.children():
child.delete(delete_children=delete_children, commit=False)
@ -1412,6 +1490,7 @@ class GroupableMixin:
# Note that this part comes after the deletion of children to prevent issues of recursion.
self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id])
if commit:
log.debug('Committing - delete tag')
self.photodb.sql.commit()
def parent(self):
@ -1435,7 +1514,6 @@ class GroupableMixin:
if not isinstance(group, type(self)):
raise TypeError('Group must also be %s' % type(self))
print('what')
if self == group:
raise ValueError('Cant join self')
@ -1446,8 +1524,10 @@ class GroupableMixin:
'''
Leave the current group and become independent.
'''
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id])
if commit:
log.debug('Committing - leave group')
self.photodb.sql.commit()
def walk_children(self):
@ -1478,6 +1558,7 @@ class Album(ObjectBase, GroupableMixin):
return
self.photodb.cur.execute('INSERT INTO album_photo_rel VALUES(?, ?)', [self.id, photo.id])
if commit:
log.debug('Committing - add photo to album')
self.photodb.sql.commit()
def add_tag_to_all(self, tag, nested_children=True):
@ -1495,6 +1576,7 @@ class Album(ObjectBase, GroupableMixin):
self.photodb.cur.execute('DELETE FROM albums WHERE id == ?', [self.id])
self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE albumid == ?', [self.id])
if commit:
log.debug('Committing - delete album')
self.photodb.sql.commit()
def edit(self, title=None, description=None, commit=True):
@ -1509,6 +1591,7 @@ class Album(ObjectBase, GroupableMixin):
self.title = title
self.description = description
if commit:
log.debug('Committing - edit album')
self.photodb.sql.commit()
def has_photo(self, photo):
@ -1553,7 +1636,7 @@ class Photo(ObjectBase):
'''
A PhotoDB entry containing information about an image file.
Photo objects cannot exist without a corresponding PhotoDB object, because
Photos are not the actual image data, just the database entry.
Photos are not the actual image data, just the database entry.
'''
def __init__(self, photodb, row_tuple):
self.photodb = photodb
@ -1568,6 +1651,7 @@ class Photo(ObjectBase):
self.ratio = row_tuple[SQL_PHOTO['ratio']]
self.area = row_tuple[SQL_PHOTO['area']]
self.bytes = row_tuple[SQL_PHOTO['bytes']]
self.duration = row_tuple[SQL_PHOTO['duration']]
self.created = row_tuple[SQL_PHOTO['created']]
self.thumbnail = row_tuple[SQL_PHOTO['thumbnail']]
self.basename = self.real_filepath.split(os.sep)[-1]
@ -1598,6 +1682,7 @@ class Photo(ObjectBase):
log.debug('Applying tag {tag:s} to photo {pho:s}'.format(tag=tag, pho=self))
self.photodb.cur.execute('INSERT INTO photo_tag_rel VALUES(?, ?)', [self.id, tag.id])
if commit:
log.debug('Committing - add photo tag')
self.photodb.sql.commit()
def albums(self):
@ -1625,6 +1710,7 @@ class Photo(ObjectBase):
self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id])
self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id])
if commit:
log.debug('Committing - delete photo')
self.photodb.sql.commit()
@time_me
@ -1659,7 +1745,7 @@ class Photo(ObjectBase):
image.save(hopeful_filepath, quality=50)
return_filepath = hopeful_filepath
elif mime == 'video':
elif mime == 'video' and ffmpeg:
print('video')
probe = ffmpeg.probe(self.real_filepath)
try:
@ -1691,6 +1777,7 @@ class Photo(ObjectBase):
self.thumbnail = return_filepath
if commit:
log.debug('Committing - generate thumbnail')
self.photodb.sql.commit()
return self.thumbnail
@ -1729,7 +1816,7 @@ class Photo(ObjectBase):
self.height = None
self.area = None
self.ratio = None
self.length = None
self.duration = None
mime = self.mimetype()
if mime == 'image':
@ -1742,11 +1829,11 @@ class Photo(ObjectBase):
image.close()
log.debug('Loaded image data for {photo:r}'.format(photo=self))
elif mime == 'video':
elif mime == 'video' and ffmpeg:
try:
probe = ffmpeg.probe(self.real_filepath)
if probe and probe.video:
self.length = probe.video.duration
self.duration = probe.video.duration
self.width = probe.video.video_width
self.height = probe.video.video_height
except:
@ -1756,7 +1843,7 @@ class Photo(ObjectBase):
try:
probe = ffmpeg.probe(self.real_filepath)
if probe and probe.audio:
self.length = probe.audio.duration
self.duration = probe.audio.duration
except:
traceback.print_exc()
@ -1764,10 +1851,11 @@ class Photo(ObjectBase):
self.area = self.width * self.height
self.ratio = round(self.width / self.height, 2)
self.photodb.cur.execute('UPDATE photos SET width=?, height=?, area=?, ratio=?, length=?, bytes=? WHERE id==?',
[self.width, self.height, self.area, self.ratio, self.length, self.bytes, self.id]
self.photodb.cur.execute('UPDATE photos SET width=?, height=?, area=?, ratio=?, duration=?, bytes=? WHERE id==?',
[self.width, self.height, self.area, self.ratio, self.duration, self.bytes, self.id],
)
if commit:
log.debug('Committing - reload metadata')
self.photodb.sql.commit()
def remove_tag(self, tag, commit=True):
@ -1781,6 +1869,7 @@ class Photo(ObjectBase):
[self.id, tag.id]
)
if commit:
log.debug('Committing - remove photo tag')
self.photodb.sql.commit()
def tags(self):
@ -1809,12 +1898,13 @@ class Tag(ObjectBase, GroupableMixin):
self.id = row_tuple[SQL_TAG['id']]
self.name = row_tuple[SQL_TAG['name']]
self.group_getter = self.photodb.get_tag
self._cached_qualified_name = None
def __eq__(self, other):
if isinstance(other, str):
return self.name == other
elif isinstance(other, Tag):
return self.id == other.id
return self.id == other.id and self.name == other.name
else:
return False
@ -1842,9 +1932,11 @@ class Tag(ObjectBase, GroupableMixin):
else:
raise TagExists(synname)
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name])
if commit:
log.debug('Committing - add synonym')
self.photodb.sql.commit()
def convert_to_synonym(self, mastertag, commit=True):
@ -1860,6 +1952,7 @@ class Tag(ObjectBase, GroupableMixin):
# Migrate the old tag's synonyms to the new one
# UPDATE is safe for this operation because there is no chance of duplicates.
self.photodb._cached_frozen_children = None
self.photodb.cur.execute(
'UPDATE tag_synonyms SET mastername = ? WHERE mastername == ?',
[mastertag.name, self.name]
@ -1876,28 +1969,34 @@ class Tag(ObjectBase, GroupableMixin):
# Then delete the relationships with the old tag
self.delete()
# Enjoy your new life as a monk.
mastertag.add_synonym(self.name, commit=False)
if commit:
log.debug('Committing - convert to synonym')
self.photodb.sql.commit()
def delete(self, delete_children=False, commit=True):
log.debug('Deleting tag {tag:r}'.format(tag=self))
self.photodb._cached_frozen_children = None
GroupableMixin.delete(self, delete_children=delete_children, commit=False)
self.photodb.cur.execute('DELETE FROM tags WHERE id == ?', [self.id])
self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE tagid == ?', [self.id])
self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE mastername == ?', [self.name])
if commit:
log.debug('Committing - delete tag')
self.photodb.sql.commit()
def qualified_name(self):
'''
Return the 'group1.group2.tag' string for this tag.
'''
if self._cached_qualified_name:
return self._cached_qualified_name
string = self.name
for parent in self.walk_parents():
string = parent.name + '.' + string
self._cached_qualified_name = string
return string
def remove_synonym(self, synname, commit=True):
@ -1912,8 +2011,10 @@ class Tag(ObjectBase, GroupableMixin):
if fetch is None:
raise NoSuchSynonym(synname)
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE name == ?', [synname])
if commit:
log.debug('Committing - remove synonym')
self.photodb.sql.commit()
def rename(self, new_name, apply_to_synonyms=True, commit=True):
@ -1931,6 +2032,8 @@ class Tag(ObjectBase, GroupableMixin):
else:
raise TagExists(new_name)
self._cached_qualified_name = None
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('UPDATE tags SET name = ? WHERE id == ?', [new_name, self.id])
if apply_to_synonyms:
self.photodb.cur.execute(
@ -1940,6 +2043,7 @@ class Tag(ObjectBase, GroupableMixin):
self.name = new_name
if commit:
log.debug('Committing - rename tag')
self.photodb.sql.commit()
def synonyms(self):
@ -1952,4 +2056,4 @@ class Tag(ObjectBase, GroupableMixin):
if __name__ == '__main__':
p = PhotoDB()
print(p)
print(p)

View File

@ -0,0 +1,3 @@
flask
gevent
pillow

View File

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -10,20 +8,36 @@
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
width="640"
height="640"
viewBox="0 0 640.00003 640"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="audio.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\audio.png"
inkscape:export-xdpi="144"
inkscape:export-ydpi="144">
sodipodi:docname="audio.svg">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="686"
inkscape:window-height="480"
id="namedview19"
showgrid="false"
inkscape:zoom="0.36875"
inkscape:cx="320"
inkscape:cy="320"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4723">
<stop
style="stop-color:#157a05;stop-opacity:1"
@ -38,13 +52,11 @@
id="clipPath5600"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path5602"
d="m 416.04743,209.57467 a 210.71427,210.71427 0 0 0 -201.63871,156.16785 210.71427,210.71427 0 0 0 148.9961,258.07232 210.71427,210.71427 0 0 0 258.07031,-148.99795 210.71427,210.71427 0 0 0 -148.9961,-258.07035 210.71427,210.71427 0 0 0 -56.4316,-7.17187 z m 0.088,10 a 200.71428,200.71428 0 0 1 53.75188,6.83 A 200.71428,200.71428 0 0 1 611.81495,472.22889 200.71428,200.71428 0 0 1 365.99079,614.15457 200.71428,200.71428 0 0 1 224.0669,368.3325 200.71428,200.71428 0 0 1 416.13517,219.57467 Z m 0.67771,10.66015 c -28.9322,-0.0158 -57.57612,6.77648 -84.07229,20.47847 -5.58051,2.89034 -7.71121,5.24233 -5.71092,8.93562 l 84.62499,146.57421 c 3.9199,6.67208 6.92071,6.5608 8.37693,-0.1625 L 463.9225,242.25987 c 1.2404,-4.75141 0.77432,-5.32621 -7.65239,-7.81065 -13.09518,-2.80222 -26.30598,-4.21013 -39.45697,-4.21472 z m 55.19328,8.75394 c -2.49601,-0.0656 -4.01546,0.93883 -4.7636,3.45495 l -43.80467,163.4825 c -1.94613,7.48962 0.25549,9.53469 6.03899,5.81055 l 146.85941,-84.78907 c 4.23688,-2.48265 4.31319,-3.22025 0.11141,-10.93556 -23.2904,-35.97209 -57.50302,-62.6885 -98.8477,-75.84771 -2.24622,-0.71332 -4.09611,-1.13676 -5.59382,-1.17566 z m -150.93155,19.66206 c -1.45873,-0.0237 -3.57203,1.00841 -7.42973,3.10929 -35.97206,23.29037 -62.69049,57.50296 -75.8496,98.84772 -1.90228,5.98981 -1.74657,9.15834 2.2793,10.35536 l 163.48439,43.80474 c 7.48959,1.94628 9.5327,-0.25337 5.8086,-6.03706 l -84.78918,-146.8593 c -1.2413,-2.11844 -2.04519,-3.19729 -3.50392,-3.22075 z m 260.03112,70.01171 c -0.7849,-0.0158 -1.62359,0.23625 -2.54681,0.73629 l -146.57616,84.62511 c -6.67213,3.91977 -6.56093,6.92076 0.16224,8.37677 l 163.8008,43.89074 c 4.75141,1.24035 5.32608,0.77437 7.8105,-7.65244 8.96741,-41.90478 3.66661,-84.98932 -16.2636,-123.52924 -2.1678,-4.18535 -4.0319,-6.43032 -6.38678,-6.44723 z m -344.04481,45.09563 c -2.28245,0 -3.07026,1.84403 -4.93359,8.16404 -8.96736,41.90477 -3.66848,84.9895 16.2617,123.52942 2.89032,5.58046 5.2424,7.71121 8.93556,5.71094 l 146.57419,-84.62495 c 6.67208,-3.91978 6.5628,-6.92092 -0.16013,-8.37709 L 239.93603,374.26994 c -1.18786,-0.31005 -2.1142,-0.51275 -2.87501,-0.51178 z m 190.67183,51.3438 c -3.55996,0.069 -4.01188,2.39073 -1.21878,6.72855 l 84.79113,146.86128 c 2.48259,4.23704 3.2182,4.31319 10.93359,0.11167 35.97198,-23.29037 62.6885,-57.50509 75.8476,-98.8497 1.90228,-5.98979 1.7466,-9.1585 -2.27929,-10.35552 L 432.3247,425.79345 c -1.87239,-0.4865 -3.4051,-0.7143 -4.5918,-0.69133 z m -16.97067,1.91211 c -1.09041,0.0477 -2.54811,0.64684 -4.35552,1.81055 l -146.8593,84.79104 c -4.23692,2.48265 -4.31319,3.21828 -0.11142,10.93359 23.2903,35.97209 57.50296,62.6885 98.84765,75.84755 5.98981,1.90226 9.15839,1.74668 10.35542,-2.27929 l 43.80668,-163.48234 c 1.33799,-5.14927 0.7152,-7.72466 -1.68359,-7.6211 z m 8.35348,2.38285 c -1.42229,0.0345 -2.53748,1.74194 -3.26558,5.1035 L 371.9615,598.30117 c -1.2405,4.75143 -0.7744,5.32621 7.65242,7.8105 41.90477,8.96746 84.98929,3.66653 123.52929,-16.26358 5.58051,-2.89034 7.7112,-5.24251 5.71091,-8.93562 l -84.625,-146.57421 c -1.95992,-3.33596 -3.69103,-4.97547 -5.11333,-4.94134 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4723"
id="linearGradient4739"
x1="196.13852"
@ -54,26 +66,6 @@
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.67124999"
inkscape:cx="199.31367"
inkscape:cy="266.33033"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px"
showguides="false" />
<metadata
id="metadata7">
<rdf:RDF>
@ -87,41 +79,33 @@
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
transform="matrix(1.5999999,0,0,1.5999999,-4.414061e-6,-1043.7794)">
<path
cx="356.42856"
cy="515.93359"
r="190.61275"
id="path4146"
style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d=""
inkscape:connector-curvature="0" />
d="" />
<path
cx="356.42856"
cy="515.93359"
r="190.61275"
id="path4148"
style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d=""
inkscape:connector-curvature="0" />
d="" />
<g
id="g4759"
transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)">
<path
id="path4176"
d="m 160.49771,775.78875 c -17.18761,15.61221 -30.85175,51.16826 -26.54481,80.55638 -5.68961,15.27438 -2.23888,26.77908 5.86857,35.70464 8.41129,9.26005 20.42276,13.46722 36.74655,8.76774 28.38226,5.82869 63.31581,-4.19178 79.95892,-19.3094 z"
style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccc" />
style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 187.71478,901.36194 c -15.6087,-1.19621 -30.19953,-6.46109 -40.11858,-17.38106 -3.59314,-3.95572 -6.38887,-8.45054 -8.4789,-13.31879"
id="path4179"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
id="path4179" />
<ellipse
transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)"
ry="71.356659"
@ -131,8 +115,6 @@
id="path4189"
style="opacity:1;fill:#bdff7f;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.10868788;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccc"
inkscape:connector-curvature="0"
id="circle5376"
d="m 167.97402,779.73201 c 12.95394,14.14301 25.67999,28.49473 38.76045,42.52164 1.09188,1.09288 1.57468,2.03532 1.12908,0.24144 -3.81326,-15.37536 -7.34553,-30.80883 -11.73933,-46.02909 -9.33962,-3.59468 -21.01947,-4.53564 -29.35443,1.94027 z m -3.40652,1.08631 c -6.35037,7.84741 -6.53271,18.97849 -3.33194,28.11951 14.61665,5.15311 29.16348,10.48467 43.81316,15.53604 2.17968,0.89623 1.61044,0.35664 0.43632,-0.95963 -13.16593,-14.74917 -26.57361,-29.28008 -39.82726,-43.95011 l -1.09034,1.25425 z m 34.52343,-0.80156 10.96907,43.65806 c 0.3333,2.09058 0.60304,1.61585 1.77113,0.51394 7.58595,-7.11871 15.71168,-13.70923 22.85068,-21.25393 -10.11646,-11.06159 -22.40233,-20.30395 -36.34759,-25.93267 z m -36.91782,33.66852 c 4.32187,13.48593 12.1718,25.57007 21.57681,36.03286 l 22.88728,-20.78947 c 1.94371,-1.55853 0.90464,-1.6299 -0.63476,-2.19321 L 161.23898,810.7949 Z m 52.1086,14.12822 44.66065,15.90394 c -4.32392,-14.54689 -12.41384,-27.7685 -22.56898,-38.9523 l -23.01225,20.90297 c -1.67223,1.35326 -1.17245,1.5415 0.92058,2.14539 z m -28.6948,23.78756 c 9.97733,11.13211 22.30826,20.25112 36.19297,25.86994 l -11.81394,-47.01872 c -0.24809,-1.18978 -0.36986,-1.35104 -1.49167,-0.33205 l -23.22275,21.09417 0.32766,0.37768 0.008,0.009 z m 27.58834,-15.54751 c 3.45815,13.76858 6.91919,27.53657 10.37846,41.30493 9.14559,4.6969 21.26842,5.13014 29.7965,-1.27057 -13.33458,-14.60073 -26.5137,-29.34346 -39.93482,-43.86507 -0.84001,-1.17611 -1.91288,-2.39494 -1.22768,-0.34375 z m 3.75391,-2.44675 37.61326,41.40874 c 7.1511,-7.87583 7.83178,-19.8138 4.31773,-29.47142 -14.6668,-5.17311 -29.26398,-10.5241 -43.96637,-15.58822 -2.34003,-1.03127 -1.12256,0.0763 -0.0843,1.25149 0.7066,0.79977 1.41314,1.59962 2.11974,2.39938 z"
style="opacity:1;fill:#6bd625;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -8,22 +6,14 @@
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
width="640"
height="640"
viewBox="0 0 640.00003 640"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="other.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\other.png"
inkscape:export-xdpi="185.87027"
inkscape:export-ydpi="185.87027">
version="1.1">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4361">
<stop
style="stop-color:#4d4d4d;stop-opacity:1"
@ -35,7 +25,6 @@
id="stop4365" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4361"
id="linearGradient4367"
x1="315.47162"
@ -45,25 +34,6 @@
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.94929084"
inkscape:cx="51.384844"
inkscape:cy="169.83481"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
@ -77,10 +47,8 @@
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
transform="matrix(1.6,0,0,1.6,-2.4414063e-5,-1043.7795)">
<rect
style="opacity:1;fill:url(#linearGradient4367);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.18531179;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4359"

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="640"
height="640"
viewBox="0 0 640.00003 640"
id="svg2"
version="1.1">
<defs
id="defs4">
<linearGradient
id="linearGradient4160">
<stop
style="stop-color:#414141;stop-opacity:1"
offset="0"
id="stop4195" />
<stop
id="stop4197"
offset="0.44465271"
style="stop-color:#a5a5a5;stop-opacity:1" />
<stop
id="stop4199"
offset="0.51481694"
style="stop-color:#cccccc;stop-opacity:1" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4201" />
</linearGradient>
<linearGradient
id="linearGradient4203">
<stop
id="stop4179"
offset="0"
style="stop-color:#414141;stop-opacity:1" />
<stop
style="stop-color:#a5a5a5;stop-opacity:1"
offset="0.44465271"
id="stop4181" />
<stop
style="stop-color:#cccccc;stop-opacity:1"
offset="0.51481694"
id="stop4183" />
<stop
id="stop4185"
offset="1"
style="stop-color:#ffffff;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient4187">
<stop
style="stop-color:#f5f5f5;stop-opacity:1"
offset="0"
id="stop4162" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4164" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient4160"
id="linearGradient4166"
x1="218.28458"
y1="950.68616"
x2="213.38019"
y2="742.12012"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient4187"
id="linearGradient4191"
x1="185.59323"
y1="394.32205"
x2="185.59323"
y2="123.97182"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(1.6,0,0,1.6,151.99998,-84.47952)"
id="g4352">
<g
id="g4324">
<g
id="g4280">
<path
style="opacity:1;fill:url(#linearGradient4191);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 0,119.99998 210.00002,0 0,280.00003 -210.00002,0 z"
id="rect4141" />
<g
id="g4254">
<path
id="path4143"
d="m 0,372 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4145"
d="m 0,352 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4147"
d="m 0,332 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4149"
d="m 0,312 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4151"
d="m 0,292 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4153"
d="m 0,272 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4155"
d="m 0,252 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4157"
d="m 0,232 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4159"
d="m 0,212 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4161"
d="m 0,192 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4163"
d="m 0,172 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#da6a8d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 20.000001,400.00001 8,0 0,-280.00002 -8,0 0,280.00002 z"
id="path4226" />
</g>
</g>
<g
id="g4268">
<path
id="rect4239"
d="m 37.453492,122.77542 135.093038,0 c 1.35923,0 2.45349,1.09426 2.45349,2.45349 l 0,17.3176 c 0,1.35923 -1.09426,2.45349 -2.45349,2.45349 l -135.093038,0 c -1.359233,0 -2.453488,-1.09426 -2.453488,-2.45349 l 0,-17.3176 c 0,-1.35923 1.094255,-2.45349 2.453488,-2.45349 z"
style="opacity:1;fill:#d1d1d1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
id="g4244">
<path
id="path4171"
d="m 105.00001,134.83513 0,-30.47246"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:9.39278793;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="rect4190"
d="m 104.41582,99.999977 1.16838,0 c 2.44636,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96945,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4192"
d="m 124.41582,99.999977 1.16838,0 c 2.44636,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96945,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4194"
d="m 144.41583,99.999977 1.16838,0 c 2.44635,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96946,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4196"
d="m 84.415818,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4198"
d="m 64.415814,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4200"
d="m 164.41583,99.999977 1.16838,0 c 2.44635,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96946,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4202"
d="m 44.415814,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</g>
<rect
y="119.99998"
x="0"
height="280.00003"
width="210.00002"
id="rect4169"
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:12.00000095;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -8,22 +6,14 @@
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
width="640"
height="640"
viewBox="0 0 640.00003 640"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="video.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\video.png"
inkscape:export-xdpi="143.99998"
inkscape:export-ydpi="143.99998">
version="1.1">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4160">
<stop
style="stop-color:#414141;stop-opacity:1"
@ -43,7 +33,6 @@
id="stop4164" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4160"
id="linearGradient4166"
x1="218.28458"
@ -52,25 +41,6 @@
y2="742.12012"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.335625"
inkscape:cx="833.60008"
inkscape:cy="34.45227"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
@ -79,22 +49,16 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
transform="matrix(1.6,0,0,1.6,-2.4414063e-5,-1043.7795)">
<path
style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:transform-center-x="-38.11878"
inkscape:transform-center-y="-2.5060727e-005"
style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 317.15321,852.36221 88.440457,984.40954 c 0,0 40.204773,-53.51798 40.204773,-132.04733 0,-78.52935 -40.204773,-132.0474 -40.204773,-132.0474 z"
id="path4157"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cczcc" />
id="path4157" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -141,12 +141,18 @@ li
{
max-height: 100%;
}
.photo_galleryview_info span
.photo_galleryview_file_metadata
{
position: absolute;
bottom: 0;
right: 0;
}
.photo_galleryview_tags
{
position: absolute;
bottom: 0;
left: 0;
}
.tag_object
{
font-size: 0.9em;

View File

@ -81,4 +81,43 @@ function bind_box_to_button(box, button)
button.click();
}
};
}
function entry_with_history_hook(box, button)
{
//console.log(event.keyCode);
if (box.entry_history === undefined)
{box.entry_history = [];}
if (box.entry_history_pos === undefined)
{box.entry_history_pos = -1;}
if (event.keyCode == 13)
{
/* Enter */
box.entry_history.push(box.value);
button.click();
box.value = "";
}
else if (event.keyCode == 38)
{
/* Up arrow */
if (box.entry_history.length == 0)
{return}
if (box.entry_history_pos == -1)
{
box.entry_history_pos = box.entry_history.length - 1;
}
else if (box.entry_history_pos > 0)
{
box.entry_history_pos -= 1;
}
box.value = box.entry_history[box.entry_history_pos];
}
else if (event.keyCode == 27)
{
box.value = "";
}
else
{
box.entry_history_pos = -1;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="640"
height="640"
viewBox="0 0 640.00004 639.99999"
id="svg2"
version="1.1">
<defs
id="defs4">
<linearGradient
id="linearGradient4499">
<stop
style="stop-color:#f31010;stop-opacity:1"
offset="0"
id="stop4501" />
<stop
style="stop-color:#ef4646;stop-opacity:1"
offset="1"
id="stop4503" />
</linearGradient>
<linearGradient
id="linearGradient4489">
<stop
style="stop-color:#1d8f0e;stop-opacity:1"
offset="0"
id="stop4491" />
<stop
style="stop-color:#2fee15;stop-opacity:1"
offset="1"
id="stop4493" />
</linearGradient>
<marker
orient="auto"
refY="0"
refX="0"
id="Tail"
style="overflow:visible">
<g
id="g4180"
transform="scale(-1.2,-1.2)"
style="fill:#74440c;fill-opacity:1;stroke:#000000;stroke-opacity:1">
<path
id="path4182"
d="M -3.8048674,-3.9585227 0.54352094,0"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4184"
d="M -1.2866832,-3.9585227 3.0617053,0"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4186"
d="M 1.3053582,-3.9585227 5.6537466,0"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4188"
d="M -3.8048674,4.1775838 0.54352094,0.21974226"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4190"
d="M -1.2866832,4.1775838 3.0617053,0.21974226"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4192"
d="M 1.3053582,4.1775838 5.6537466,0.21974226"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
</g>
</marker>
<linearGradient
xlink:href="#linearGradient4489"
id="linearGradient4495"
x1="280.82242"
y1="1056.675"
x2="36.084953"
y2="669.78662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.997383,-786.40284)" />
<linearGradient
xlink:href="#linearGradient4499"
id="linearGradient4505"
x1="336.3808"
y1="1003.8561"
x2="105.84174"
y2="647.27222"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.996343,-786.40298)" />
<linearGradient
id="linearGradient4361">
<stop
id="stop4363"
offset="0"
style="stop-color:#4d4d4d;stop-opacity:1" />
<stop
id="stop4365"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)"
gradientUnits="userSpaceOnUse"
y2="699.85175"
x2="204.01379"
y1="1037.2178"
x1="315.47162"
id="linearGradient4367"
xlink:href="#linearGradient4361" />
<linearGradient
xlink:href="#linearGradient4499"
id="linearGradient4505-3"
x1="336.3808"
y1="1003.8561"
x2="105.84174"
y2="647.27222"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.996343,-786.40298)" />
<linearGradient
xlink:href="#linearGradient4489"
id="linearGradient4495-0"
x1="280.82242"
y1="1056.675"
x2="36.084953"
y2="669.78662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.997383,-786.40284)" />
<linearGradient
xlink:href="#linearGradient4499"
id="linearGradient4505-3-9"
x1="336.3808"
y1="1003.8561"
x2="105.84174"
y2="647.27222"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.220263,0,0,1.220263,66.826923,-710.20939)" />
<linearGradient
xlink:href="#linearGradient4489"
id="linearGradient4495-0-1"
x1="280.82242"
y1="1056.675"
x2="36.084953"
y2="669.78662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.220263,0,0,1.220263,66.827863,-710.20926)" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path4480"
d="m 573.17315,444.37019 c -48.25919,77.78171 -134.42937,129.57907 -232.69753,129.57907 -151.13202,0 -273.648697,-122.51669 -273.648697,-273.64869 0,-90.63158 44.059897,-170.97205 111.928527,-220.771487 6.51438,-4.779996 13.24791,-9.279034 20.18309,-13.478366 z"
style="opacity:1;fill:url(#linearGradient4505-3-9);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:19.16129684;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="circle4482"
d="M 178.24792,78.766505 C 107.30648,130.14717 66.452241,211.94323 66.319393,299.53686 66.319375,450.66888 189.34458,573.94828 340.47661,573.94828 428.2425,573.84456 511.5918,532.18966 562.97034,461.03416 c -46.64082,33.96568 -103.78292,51.78066 -161.48062,51.90097 -151.13198,0 -274.15719,-123.27939 -274.15718,-274.41142 0.095,-57.55121 17.12911,-113.1671 50.91538,-159.757205 z"
style="opacity:1;fill:url(#linearGradient4495-0-1);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:19.16129684;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:16.25600243;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 573.17315,444.37019 c -48.25919,77.78171 -134.42937,129.57907 -232.69753,129.57907 -151.13202,0 -273.648697,-122.51669 -273.648697,-273.64869 0,-90.63158 44.059897,-170.97205 111.928527,-220.771487 6.51438,-4.779996 13.24791,-9.279034 20.18309,-13.478366 z"
id="path4252" />
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -3,7 +3,7 @@
<head>
{% import "photo_object.html" as photo_object %}
{% import "header.html" as header %}
<title>Album {{album.title}}</title>
<title>Album {{album["title"]}}</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css">
</head>
@ -18,20 +18,20 @@
<body>
{{header.make_header()}}
<div id="content_body">
<h2>{{album.title}}</h2>
{% set parent=album.parent() %}
<h2>{{album["title"]}}</h2>
{% set parent=album["parent"] %}
{% if parent %}
<h3>Parent: <a href="/album/{{parent.id}}">{{parent.title}}</a></h3>
<h3>Parent: <a href="/album/{{parent["id"]}}">{{parent.title}}</a></h3>
{% endif %}
{% if child_albums %}
<h3>Sub-albums</h3>
<ul>
{% for album in child_albums %}
<li><a href="/album/{{album.id}}">
{% if album.title %}
{{album.title}}
<li><a href="/album/{{album["id"]}}">
{% if album["title"] %}
{{album["title"]}}
{% else %}
{{album.id}}
{{album["id"]}}
{% endif %}</a>
</li>
{% endfor %}

View File

@ -13,6 +13,7 @@
/* Override common.css */
flex: 1;
height: 100%;
width: 100%;
}
#left
{
@ -45,20 +46,23 @@
flex-direction: column;
justify-content: center;
align-items: center;
max-height: 100%;
max-width: 100%;
height: 100%;
width: 100%;
}
.photo_object a
{
max-height: 100%;
max-width: 100%;
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
max-height: 100%;
}
.photo_object img
{
height: auto;
height: 100%;
max-height: 100%;
max-width: 100%;
}
.photo_object audio
{
@ -80,10 +84,10 @@
<!-- TAG INFO -->
<h4>Tags</h4>
<ul id="this_tags">
{% for tag in tags %}
{% for tag in photo['tags'] %}
<li>
<a class="tag_object" href="/search?tag_musts={{tag.name}}">{{tag.qualified_name()}}</a>
<button class="remove_tag_button" onclick="remove_photo_tag('{{photo.id}}', '{{tag.name}}', receive_callback);"></button>
<a class="tag_object" href="/search?tag_musts={{tag["name"]}}">{{tag["qualified_name"]}}</a>
<button class="remove_tag_button" onclick="remove_photo_tag('{{photo["id"]}}', '{{tag["name"]}}', receive_callback);"></button>
</li>
{% endfor %}
<li>
@ -95,22 +99,24 @@
<!-- METADATA & DOWNLOAD -->
<h4>File info</h4>
<ul id="metadata">
{% if photo.width %}
<li>{{photo.width}}x{{photo.height}} px</li>
<li>{{photo.ratio}} aspect ratio</li>
<li>{{photo.bytestring()}}</li>
{% if photo["width"] %}
<li>{{photo["width"]}}x{{photo["height"]}} px</li>
<li>{{photo["ratio"]}} aspect ratio</li>
<li>{{photo["bytestring"]}}</li>
{% endif %}
<li><a href="/file/{{photo.id}}.{{photo.extension}}?download=1">Download as {{photo.id}}.{{photo.extension}}</a></li>
<li><a href="/file/{{photo.id}}.{{photo.extension}}?download=1&original_filename=1">Download as "{{photo.basename}}"</a></li>
<li>{{photo["duration"]}}</li>
{% if photo["duration"] %}
{% endif %}
<li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1">Download as {{photo["id"]}}.{{photo["extension"]}}</a></li>
<li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1&original_filename=1">Download as "{{photo["filename"]}}"</a></li>
</ul>
<!-- CONTAINING ALBUMS -->
{% set albums=photo.albums() %}
{% if albums %}
{% if photo["albums"] %}
<h4>Albums containing this photo</h4>
<ul id="containing albums">
{% for album in albums %}
<li><a href="/album/{{album.id}}">{{album.title}}</a></li>
{% for album in photo["albums"] %}
<li><a href="/album/{{album["id"]}}">{{album["title"]}}</a></li>
{% endfor %}
{% endif %}
</ul>
@ -121,13 +127,14 @@
<div id="right">
<div class="photo_object">
{% set filename = photo.id + "." + photo.extension %}
{% set filename = photo["id"] + "." + photo["extension"] %}
{% set link = "/file/" + filename %}
{% set mimetype=photo.mimetype() %}
{% set mimetype=photo["mimetype"] %}
{% if mimetype == "image" %}
<a target="_blank" href="{{link}}"><img src="{{link}}"></a>
<!-- <a target="_blank" href="{{link}}"><img src="{{link}}"></a> -->
<img src="{{link}}">
{% elif mimetype == "video" %}
<video src="{{link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video>
<video src="{{link}}" controls preload=none {%if photo["has_thumbnail"]%}poster="/thumbnail/{{photo["id"]}}.jpg"{%endif%}></video>
{% elif mimetype == "audio" %}
<audio src="{{link}}" controls></audio>
{% else %}
@ -140,10 +147,10 @@
</html>
<script type="text/javascript">
var box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button');
var add_tag_box = document.getElementById('add_tag_textbox');
var add_tag_button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area');
bind_box_to_button(box, button);
add_tag_box.onkeydown = function(){entry_with_history_hook(add_tag_box, add_tag_button)};
function receive_callback(response)
{
@ -170,7 +177,7 @@ function receive_callback(response)
}
function submit_tag(callback)
{
add_photo_tag('{{photo.id}}', box.value, callback);
box.value='';
add_photo_tag('{{photo["id"]}}', add_tag_box.value, callback);
add_tag_box.value = "";
}
</script>

View File

@ -1,29 +1,47 @@
{% set basics =
{
"audio": "audio",
"txt": "txt",
"video": "video",
}
%}
{% macro galleryview(photo) %}
<div class="photo_galleryview">
<div class="photo_galleryview_thumb">
<a target="_blank" href="/photo/{{photo.id}}">
<a target="_blank" href="/photo/{{photo["id"]}}">
<img height="150"
{% if photo.thumbnail %}
src="/thumbnail/{{photo.id}}.jpg"
{% if photo["has_thumbnail"] %}
src="/thumbnail/{{photo["id"]}}.jpg"
{% else %}
{% set mimetype = photo.mimetype() %}
{% if mimetype == 'video' %}
src="/static/basic_thumbnails/video.png"
{% elif mimetype == 'audio' %}
src="/static/basic_thumbnails/audio.png"
{% else %}
src="/static/basic_thumbnails/other.png"
{% endif %}
{% set choice =
photo['extension'] if photo['extension'] in basics else
photo['mimetype'] if photo['mimetype'] in basics else
'other'
%}
src="/static/basic_thumbnails/{{choice}}.png"
{% endif %}
</a>
</div>
<div class="photo_galleryview_info">
<a target="_blank" href="/photo/{{photo.id}}">{{photo.basename}}</a>
<span>
{% if photo.width %}
{{photo.width}}x{{photo.height}}
<a target="_blank" href="/photo/{{photo["id"]}}">{{photo["filename"]}}</a>
<span class="photo_galleryview_file_metadata">
{% if photo["width"] %}
{{photo["width"]}}x{{photo["height"]}},
{% endif %}
{{photo.bytestring()}}
{% if photo["duration"] %}
{{photo["duration"]}},
{% endif %}
{{photo["bytestring"]}}
</span>
<span class="photo_galleryview_tags">
{% if photo["tags"] %}
{% set tags=[] %}
{% for tag in photo["tags"] %}
{% do tags.append(tag["name"]) %}
{% endfor %}
<span title="{{", ".join(tags)}}">T</span>
{% endif %}
</span>
</div>
</div>

View File

@ -13,9 +13,13 @@ a
{
width: 50%;
height: 40px;
background-color: #ffffd4;
background-color: rgba(0, 0, 0, 0.1);
margin: 8px;
}
a:hover
{
background-color: #ffffd4;
}
</style>
<head>

View File

@ -94,14 +94,14 @@ form
{% macro create_orderby_li(selected_column, selected_sorter) %}
<li class="search_builder_orderby_li">
<select>
<option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option>
<option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option>
<option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option>
<option value="height" {%if selected_column=="height"%}selected{%endif%} >Height</option>
<option value="ratio" {%if selected_column=="ratio"%}selected{%endif%} >Aspect Ratio</option>
<option value="bytes" {%if selected_column=="bytes"%}selected{%endif%} >File size</option>
<option value="length" {%if selected_column=="length"%}selected{%endif%} >Duration</option>
<option value="random" {%if selected_column=="random"%}selected{%endif%} >Random</option>
<option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option>
<option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option>
<option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option>
<option value="height" {%if selected_column=="height"%}selected{%endif%} >Height</option>
<option value="ratio" {%if selected_column=="ratio"%}selected{%endif%} >Aspect Ratio</option>
<option value="bytes" {%if selected_column=="bytes"%}selected{%endif%} >File size</option>
<option value="duration" {%if selected_column=="duration"%}selected{%endif%} >Duration</option>
<option value="random" {%if selected_column=="random"%}selected{%endif%} >Random</option>
</select>
<select>
<option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option>
@ -149,8 +149,7 @@ form
<ul id="search_builder_orderby_ul">
{% if "orderby" in search_kwargs and search_kwargs["orderby"] %}
{% for orderby in search_kwargs["orderby"] %}
{% set column=orderby.split(" ")[0] %}
{% set sorter=orderby.split(" ")[1] %}
{% set column, sorter=orderby.split(" ") %}
{{ create_orderby_li(selected_column=column, selected_sorter=sorter) }}
{% endfor %}
{% else %}
@ -161,24 +160,53 @@ form
</div>
<br>
<form id="search_builder_form" action="" onsubmit="return submit_search();">
<input type="text"
<span>Min-max values</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['area']%}{{search_kwargs['area']}}{%endif%}"
name="area" placeholder="Area: 1m-2m">
<input type="text" class="basic_param"
value="{%if search_kwargs['width']%}{{search_kwargs['width']}}{%endif%}"
name="width" placeholder="Width: 1k-2k">
<input type="text" class="basic_param"
value="{%if search_kwargs['height']%}{{search_kwargs['height']}}{%endif%}"
name="height" placeholder="Height: 1k-2k">
<input type="text" class="basic_param"
value="{%if search_kwargs['ratio']%}{{search_kwargs['ratio']}}{%endif%}"
name="ratio" placeholder="Aspect Ratio: 1.7-2">
<input type="text" class="basic_param"
value="{%if search_kwargs['bytes']%}{{search_kwargs['bytes']}}{%endif%}"
name="bytes" placeholder="File Size: 1mb-2mb">
<input type="text" class="basic_param"
value="{%if search_kwargs['duration']%}{{search_kwargs['duration']}}{%endif%}"
name="duration" placeholder="Duration: 10:00-20:00">
<br>
<span>Other filters</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}"
name="mimetype" placeholder="Mimetype(s)">
<input type="text"
<input type="text" class="basic_param"
value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}"
name="extension" placeholder="Extension(s)">
<input type="text"
<input type="text" class="basic_param"
value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}"
name="extension_not" placeholder="Forbid extension(s)">
<select name="limit">
<select name="limit" class="basic_param">
<option value="20" {%if search_kwargs['limit'] == 20%}selected{%endif%}>20 items</option>
<option value="50" {%if search_kwargs['limit'] == 50%}selected{%endif%}>50 items</option>
<option value="100" {%if search_kwargs['limit'] == 100%}selected{%endif%}>100 items</option>
</select>
<select name="has_tags">
<select name="has_tags" class="basic_param">
<option value="" {%if search_kwargs['has_tags'] == None %}selected{%endif%}>Tagged and untagged</option>
<option value="yes"{%if search_kwargs['has_tags'] == True %}selected{%endif%}>Tagged only</option>
<option value="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option>
@ -189,7 +217,7 @@ form
<span>Tags on this page (click to join query):</span>
<ul>
{% for tag in total_tags %}
<li><a href="" class="tag_object">{{tag._cached_qualname}}</a></li>
<li><a href="" class="tag_object">{{tag}}</a></li>
{% endfor %}
</ul>
{% endif %}
@ -314,6 +342,18 @@ function orderby_remove_hook(button)
ul.removeChild(li);
}
}
function simplify_tagnames(tags)
{
var new_tags = [];
for (var index = 0; index < tags.length; index += 1)
{
var tag = tags[index];
tag = tag.split(".");
tag = tag[tag.length - 1];
new_tags.push(tag);
}
return new_tags;
}
function submit_search()
{
/*
@ -322,13 +362,13 @@ function submit_search()
var url = window.location.origin + "/search";
var parameters = [];
var has_tag_params = false;
var musts = inputted_musts.join(",");
var musts = simplify_tagnames(inputted_musts).join(",");
if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;}
var mays = inputted_mays.join(",");
var mays = simplify_tagnames(inputted_mays).join(",");
if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;}
var forbids = inputted_forbids.join(",");
var forbids = simplify_tagnames(inputted_forbids).join(",");
if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;}
var expression = document.getElementsByName("tag_expression")[0].value;
@ -339,10 +379,10 @@ function submit_search()
has_tag_params=true;
}
var basic_inputs = ["mimetype", "extension", "extension_not", "limit", "has_tags"];
var basic_inputs = document.getElementsByClassName("basic_param");
for (var index = 0; index < basic_inputs.length; index += 1)
{
var boxname = basic_inputs[index];
var boxname = basic_inputs[index].name;
var box = document.getElementsByName(boxname)[0];
var value = box.value;
if (boxname == "has_tags" && has_tag_params && value == "no")
@ -414,8 +454,8 @@ function tag_input_hook(box, inputted_list, li_class)
value = value.split(".");
value = value[value.length-1];
value = value.split("+")[0];
value = value.replace(" ", "_");
value = value.replace("-", "_");
value = value.replace(new RegExp(" ", 'g'), "_");
value = value.replace(new RegExp("-", 'g'), "_");
if (!(value in QUALNAME_MAP))
{
return;

View File

@ -91,7 +91,7 @@ body
var box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area');
bind_box_to_button(box, button);
box.onkeydown = function(){entry_with_history_hook(box, button)};
function receive_callback(responses)
{
@ -115,6 +115,9 @@ function receive_callback(responses)
if (action == "new_tag")
{message_text = "Created tag " + tagname;}
else if (action == "new_synonym")
{message_text = "New synonym " + tagname;}
else if (action == "existing_tag")
{message_text = "Existing tag " + tagname;}
@ -137,6 +140,6 @@ function receive_callback(responses)
function submit_tag(callback)
{
create_tag(box.value, callback);
box.value='';
box.value = "";
}
</script>

View File

@ -1,12 +1,13 @@
import os
import phototagger
import unittest
import random
class PhotoDBTest(unittest.TestCase):
def setUp(self):
self.P = phototagger.PhotoDB(':memory:')
class AlbumTest(PhotoDBTest):
'''
Test the creation and properties of albums
@ -55,6 +56,7 @@ class PhotoTest(PhotoDBTest):
def test_reload_metadata(self):
pass
class TagTest(PhotoDBTest):
'''
Test the creation and properties of tags
@ -89,6 +91,7 @@ class TagTest(PhotoDBTest):
self.assertRaises(phototagger.TagTooShort, tag.rename, '??')
tag.rename(tag.name) # does nothing
class SearchTest(PhotoDBTest):
def search_extension(self):
pass
@ -99,6 +102,7 @@ class SearchTest(PhotoDBTest):
def search_tags(self):
pass
class SynonymTest(PhotoDBTest):
'''
Test the creation and management of synonyms
@ -136,6 +140,7 @@ class SynonymTest(PhotoDBTest):
tag.add_synonym('test get syns3')
self.assertEqual(len(tag.synonyms()), 3)
class AlbumGroupTest(PhotoDBTest):
'''
Test the relationships between albums as they form and leave groups
@ -155,6 +160,7 @@ class AlbumGroupTest(PhotoDBTest):
def test_album_parents(self):
pass
class TagGroupTest(PhotoDBTest):
'''
Test the relationships between tags as they form and leave groups
@ -188,6 +194,7 @@ class AlbumPhotoTest(PhotoDBTest):
def test_remove_photo(self):
pass
class PhotoTagTest(PhotoDBTest):
'''
Test the relationships between photos and tags