checkpoint
add photo and search css for narrow screens; incorporate new expressionmatch kit; entry_with_history cursor moves to end; albums indicate total filesize; etc
This commit is contained in:
parent
80cb66b825
commit
c80e2003ff
16 changed files with 266 additions and 743 deletions
|
@ -4,8 +4,8 @@ Etiquette
|
||||||
This is the readme file.
|
This is the readme file.
|
||||||
|
|
||||||
### To do list
|
### To do list
|
||||||
|
- At the moment I don't like the way that warnings and exceptions are so far apart, and need to be updated individually. Consider moving the warning strings to be class properties of the matching exceptions.
|
||||||
- User account system, permission levels, private pages.
|
- User account system, permission levels, private pages.
|
||||||
- Bookmark system. Maybe the ability to submit URLs as photo objects.
|
|
||||||
- Generalize the filename expression filter so it can work with any strings.
|
- Generalize the filename expression filter so it can work with any strings.
|
||||||
- Improve the "tags on this page" list. Maybe add separate buttons for must/may/forbid on each.
|
- Improve the "tags on this page" list. Maybe add separate buttons for must/may/forbid on each.
|
||||||
- Some way for the database to re-identify a file that was moved / renamed (lost & found). Maybe file hash of the first few mb is good enough.
|
- Some way for the database to re-identify a file that was moved / renamed (lost & found). Maybe file hash of the first few mb is good enough.
|
||||||
|
@ -21,10 +21,3 @@ This is the readme file.
|
||||||
- **[removal]** An old feature was removed.
|
- **[removal]** An old feature was removed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- 2016 11 28
|
|
||||||
- **[addition]** Added `etiquette_upgrader.py`. When an update causes the anatomy of the etiquette database to change, I will increment the `phototagger.DATABASE_VERSION` variable, and add a new function to this script that should automatically make all the necessary changes. Until the database is upgraded, phototagger will not start. Don't forget to make backups just in case.
|
|
||||||
|
|
||||||
- 2016 11 05
|
|
||||||
- **[addition]** Added the ability to download an album as a `.tar` file. No compression is used. I still need to do more experiments to make sure this is working perfectly.
|
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,8 @@ ERROR_INVALID_ACTION = 'Invalid action'
|
||||||
ERROR_NO_SUCH_TAG = 'Doesn\'t exist'
|
ERROR_NO_SUCH_TAG = 'Doesn\'t exist'
|
||||||
ERROR_NO_TAG_GIVEN = 'No tag name supplied'
|
ERROR_NO_TAG_GIVEN = 'No tag name supplied'
|
||||||
ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself'
|
ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself'
|
||||||
ERROR_TAG_TOO_SHORT = 'Not enough valid chars'
|
ERROR_TAG_TOO_LONG = '{tag} is too long'
|
||||||
|
ERROR_TAG_TOO_SHORT = '{tag} has too few valid chars'
|
||||||
ERROR_RECURSIVE_GROUPING = 'Recursive grouping'
|
ERROR_RECURSIVE_GROUPING = 'Recursive grouping'
|
||||||
WARNING_MINMAX_INVALID = 'Field "{field}": "{value}" is not a valid request. 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_MINMAX_OOO = 'Field "{field}": minimum "{min}" maximum "{max}" are out of order. Ignored.'
|
||||||
|
|
|
@ -11,6 +11,7 @@ from voussoirkit import bytestring
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
from voussoirkit import spinal
|
from voussoirkit import spinal
|
||||||
|
|
||||||
|
|
||||||
class ObjectBase:
|
class ObjectBase:
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (
|
return (
|
||||||
|
@ -101,7 +102,8 @@ class GroupableMixin:
|
||||||
# Lift children
|
# Lift children
|
||||||
parent = self.parent()
|
parent = self.parent()
|
||||||
if parent is None:
|
if parent is None:
|
||||||
# Since this group was a root, children become roots by removing the row.
|
# Since this group was a root, children become roots by removing
|
||||||
|
# the row.
|
||||||
cur.execute('DELETE FROM tag_group_rel WHERE parentid == ?', [self.id])
|
cur.execute('DELETE FROM tag_group_rel WHERE parentid == ?', [self.id])
|
||||||
else:
|
else:
|
||||||
# Since this group was a child, its parent adopts all its children.
|
# Since this group was a child, its parent adopts all its children.
|
||||||
|
@ -109,7 +111,8 @@ class GroupableMixin:
|
||||||
'UPDATE tag_group_rel SET parentid == ? WHERE parentid == ?',
|
'UPDATE tag_group_rel SET parentid == ? WHERE parentid == ?',
|
||||||
[parent.id, self.id]
|
[parent.id, self.id]
|
||||||
)
|
)
|
||||||
# Note that this part comes after the deletion of children to prevent issues of recursion.
|
# Note that this part comes after the deletion of children to prevent
|
||||||
|
# issues of recursion.
|
||||||
cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id])
|
cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id])
|
||||||
if commit:
|
if commit:
|
||||||
self.photodb.log.debug('Committing - delete tag')
|
self.photodb.log.debug('Committing - delete tag')
|
||||||
|
@ -269,6 +272,20 @@ class Album(ObjectBase, GroupableMixin):
|
||||||
self.photodb.log.debug('Committing - remove photo from album')
|
self.photodb.log.debug('Committing - remove photo from album')
|
||||||
self.photodb.commit()
|
self.photodb.commit()
|
||||||
|
|
||||||
|
def sum_bytes(self, recurse=True, string=False):
|
||||||
|
if recurse:
|
||||||
|
photos = self.walk_photos()
|
||||||
|
else:
|
||||||
|
photos = self.photos()
|
||||||
|
|
||||||
|
total = sum(photo.bytes for photo in photos)
|
||||||
|
|
||||||
|
if string:
|
||||||
|
return bytestring.bytestring(total)
|
||||||
|
else:
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
def walk_photos(self):
|
def walk_photos(self):
|
||||||
yield from self.photos()
|
yield from self.photos()
|
||||||
children = self.walk_children()
|
children = self.walk_children()
|
||||||
|
@ -642,7 +659,7 @@ class Photo(ObjectBase):
|
||||||
new_path = old_path.parent.with_child(new_filename)
|
new_path = old_path.parent.with_child(new_filename)
|
||||||
else:
|
else:
|
||||||
new_path = pathclass.Path(new_filename)
|
new_path = pathclass.Path(new_filename)
|
||||||
new_path.correct_case()
|
#new_path.correct_case()
|
||||||
|
|
||||||
self.photodb.log.debug(old_path)
|
self.photodb.log.debug(old_path)
|
||||||
self.photodb.log.debug(new_path)
|
self.photodb.log.debug(new_path)
|
||||||
|
@ -655,9 +672,11 @@ class Photo(ObjectBase):
|
||||||
os.makedirs(new_path.parent.absolute_path, exist_ok=True)
|
os.makedirs(new_path.parent.absolute_path, exist_ok=True)
|
||||||
|
|
||||||
if new_path != old_path:
|
if new_path != old_path:
|
||||||
# This is different than the absolute == absolute check above, because this normalizes
|
# This is different than the absolute == absolute check above,
|
||||||
# the paths. It's possible on case-insensitive systems to have the paths point to the
|
# because this normalizes the paths. It's possible on
|
||||||
# same place while being differently cased, thus we couldn't make the intermediate link.
|
# case-insensitive systems to have the paths point to the same place
|
||||||
|
# while being differently cased, thus we couldn't make the
|
||||||
|
# intermediate link.
|
||||||
try:
|
try:
|
||||||
os.link(old_path.absolute_path, new_path.absolute_path)
|
os.link(old_path.absolute_path, new_path.absolute_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -671,7 +690,8 @@ class Photo(ObjectBase):
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
if new_path == old_path:
|
if new_path == old_path:
|
||||||
# If they are equivalent but differently cased paths, just rename.
|
# If they are equivalent but differently cased paths, just
|
||||||
|
# rename.
|
||||||
os.rename(old_path.absolute_path, new_path.absolute_path)
|
os.rename(old_path.absolute_path, new_path.absolute_path)
|
||||||
else:
|
else:
|
||||||
# Delete the original hardlink or copy.
|
# Delete the original hardlink or copy.
|
||||||
|
@ -900,6 +920,7 @@ class User(ObjectBase):
|
||||||
rep = 'User:{username}'.format(username=self.username)
|
rep = 'User:{username}'.format(username=self.username)
|
||||||
return rep
|
return rep
|
||||||
|
|
||||||
|
|
||||||
class WarningBag:
|
class WarningBag:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.warnings = set()
|
self.warnings = set()
|
||||||
|
|
|
@ -16,6 +16,7 @@ from . import helpers
|
||||||
from . import objects
|
from . import objects
|
||||||
from . import searchhelpers
|
from . import searchhelpers
|
||||||
|
|
||||||
|
from voussoirkit import expressionmatch
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
from voussoirkit import safeprint
|
from voussoirkit import safeprint
|
||||||
from voussoirkit import spinal
|
from voussoirkit import spinal
|
||||||
|
@ -161,76 +162,6 @@ def raise_no_such_thing(exception_class, thing_id=None, thing_name=None, comment
|
||||||
message = ''
|
message = ''
|
||||||
raise exception_class(message)
|
raise exception_class(message)
|
||||||
|
|
||||||
def searchfilter_expression(photo_tags, expression, frozen_children, token_normalizer, warning_bag=None):
|
|
||||||
photo_tags = set(tag.name for tag in photo_tags)
|
|
||||||
operator_stack = collections.deque()
|
|
||||||
operand_stack = collections.deque()
|
|
||||||
|
|
||||||
expression = expression.replace('-', ' ')
|
|
||||||
expression = expression.strip()
|
|
||||||
if not expression:
|
|
||||||
return False
|
|
||||||
expression = expression.replace('(', ' ( ')
|
|
||||||
expression = expression.replace(')', ' ) ')
|
|
||||||
while ' ' in expression:
|
|
||||||
expression = expression.replace(' ', ' ')
|
|
||||||
tokens = [token for token in expression.split(' ') if token]
|
|
||||||
has_operand = False
|
|
||||||
can_shortcircuit = False
|
|
||||||
|
|
||||||
for token in tokens:
|
|
||||||
#print(token, end=' ', flush=True)
|
|
||||||
if can_shortcircuit and token != ')':
|
|
||||||
continue
|
|
||||||
|
|
||||||
if token not in constants.EXPRESSION_OPERATORS:
|
|
||||||
try:
|
|
||||||
token = token_normalizer(token)
|
|
||||||
value = any(option in photo_tags for option in frozen_children[token])
|
|
||||||
except KeyError:
|
|
||||||
if warning_bag:
|
|
||||||
warning_bag.add(constants.WARNING_NO_SUCH_TAG.format(tag=token))
|
|
||||||
else:
|
|
||||||
raise exceptions.NoSuchTag(token)
|
|
||||||
return False
|
|
||||||
operand_stack.append(value)
|
|
||||||
if has_operand:
|
|
||||||
operate(operand_stack, operator_stack)
|
|
||||||
has_operand = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
if token == '(':
|
|
||||||
has_operand = False
|
|
||||||
|
|
||||||
if token == ')':
|
|
||||||
if not can_shortcircuit:
|
|
||||||
while operator_stack[-1] != '(':
|
|
||||||
operate(operand_stack, operator_stack)
|
|
||||||
operator_stack.pop()
|
|
||||||
has_operand = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
can_shortcircuit = (
|
|
||||||
has_operand and
|
|
||||||
(
|
|
||||||
(operand_stack[-1] == 0 and token == 'AND') or
|
|
||||||
(operand_stack[-1] == 1 and token == 'OR')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if can_shortcircuit:
|
|
||||||
if operator_stack and operator_stack[-1] == '(':
|
|
||||||
operator_stack.pop()
|
|
||||||
continue
|
|
||||||
|
|
||||||
operator_stack.append(token)
|
|
||||||
#time.sleep(.3)
|
|
||||||
#print()
|
|
||||||
while len(operand_stack) > 1 or len(operator_stack) > 0:
|
|
||||||
operate(operand_stack, operator_stack)
|
|
||||||
#print(operand_stack)
|
|
||||||
success = operand_stack.pop()
|
|
||||||
return success
|
|
||||||
|
|
||||||
def searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children):
|
def searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children):
|
||||||
if tag_musts and not all(any(option in photo_tags for option in frozen_children[must]) for must in tag_musts):
|
if tag_musts and not all(any(option in photo_tags for option in frozen_children[must]) for must in tag_musts):
|
||||||
#print('Failed musts')
|
#print('Failed musts')
|
||||||
|
@ -728,7 +659,16 @@ class PDBPhotoMixin:
|
||||||
searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag)
|
searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag)
|
||||||
|
|
||||||
orderby = searchhelpers.normalize_orderby(orderby)
|
orderby = searchhelpers.normalize_orderby(orderby)
|
||||||
query = searchhelpers.build_query(orderby)
|
notnulls = []
|
||||||
|
if extension or mimetype:
|
||||||
|
notnulls.append('extension')
|
||||||
|
if width or height or ratio or area:
|
||||||
|
notnulls.append('width')
|
||||||
|
if bytes:
|
||||||
|
notnulls.append('bytes')
|
||||||
|
if duration:
|
||||||
|
notnulls.append('duration')
|
||||||
|
query = searchhelpers.build_query(orderby, notnulls)
|
||||||
print(query)
|
print(query)
|
||||||
generator = helpers.select_generator(self.sql, query)
|
generator = helpers.select_generator(self.sql, query)
|
||||||
|
|
||||||
|
@ -774,6 +714,12 @@ class PDBPhotoMixin:
|
||||||
else:
|
else:
|
||||||
frozen_children = self.export_tags(tag_export_totally_flat)
|
frozen_children = self.export_tags(tag_export_totally_flat)
|
||||||
self._cached_frozen_children = frozen_children
|
self._cached_frozen_children = frozen_children
|
||||||
|
|
||||||
|
if tag_expression:
|
||||||
|
expression_tree = expressionmatch.ExpressionTree.parse(tag_expression)
|
||||||
|
expression_tree.map(self.normalize_tagname)
|
||||||
|
expression_matcher = searchhelpers.tag_expression_matcher_builder(frozen_children, warning_bag)
|
||||||
|
|
||||||
photos_received = 0
|
photos_received = 0
|
||||||
|
|
||||||
# LET'S GET STARTED
|
# LET'S GET STARTED
|
||||||
|
@ -791,7 +737,7 @@ class PDBPhotoMixin:
|
||||||
(photo.extension in extension_not)
|
(photo.extension in extension_not)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (ext_fail):
|
if ext_fail:
|
||||||
#print('Failed extension_not')
|
#print('Failed extension_not')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -822,7 +768,7 @@ class PDBPhotoMixin:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (has_tags is not None) or is_tagsearch:
|
if (has_tags is not None) or is_tagsearch:
|
||||||
photo_tags = photo.tags()
|
photo_tags = set(photo.tags())
|
||||||
|
|
||||||
if has_tags is False and len(photo_tags) > 0:
|
if has_tags is False and len(photo_tags) > 0:
|
||||||
#print('Failed has_tags=False')
|
#print('Failed has_tags=False')
|
||||||
|
@ -832,15 +778,11 @@ class PDBPhotoMixin:
|
||||||
#print('Failed has_tags=True')
|
#print('Failed has_tags=True')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
photo_tags = set(photo_tags)
|
|
||||||
|
|
||||||
if tag_expression:
|
if tag_expression:
|
||||||
success = searchfilter_expression(
|
success = expression_tree.evaluate(
|
||||||
photo_tags=photo_tags,
|
photo_tags,
|
||||||
expression=tag_expression,
|
match_function=expression_matcher,
|
||||||
frozen_children=frozen_children,
|
|
||||||
token_normalizer=self.normalize_tagname,
|
|
||||||
warning_bag=warning_bag,
|
|
||||||
)
|
)
|
||||||
if not success:
|
if not success:
|
||||||
#print('Failed tag expression')
|
#print('Failed tag expression')
|
||||||
|
@ -872,7 +814,7 @@ class PDBPhotoMixin:
|
||||||
yield warning_bag
|
yield warning_bag
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
print(end_time - start_time)
|
print('Search results took:', end_time - start_time)
|
||||||
|
|
||||||
|
|
||||||
class PDBTagMixin:
|
class PDBTagMixin:
|
||||||
|
@ -954,7 +896,7 @@ class PDBTagMixin:
|
||||||
tag = objects.Tag(self, [tagid, tagname])
|
tag = objects.Tag(self, [tagid, tagname])
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
def normalize_tagname(self, tagname):
|
def normalize_tagname(self, tagname, warning_bag=None):
|
||||||
'''
|
'''
|
||||||
Tag names can only consist of characters defined in the config.
|
Tag names can only consist of characters defined in the config.
|
||||||
The given tagname is lowercased, gets its spaces and hyphens
|
The given tagname is lowercased, gets its spaces and hyphens
|
||||||
|
@ -968,10 +910,18 @@ class PDBTagMixin:
|
||||||
tagname = ''.join(tagname)
|
tagname = ''.join(tagname)
|
||||||
|
|
||||||
if len(tagname) < self.config['min_tag_name_length']:
|
if len(tagname) < self.config['min_tag_name_length']:
|
||||||
|
if warning_bag is not None:
|
||||||
|
warning_bag.add(constants.WARNING_TAG_TOO_SHORT.format(tag=tagname))
|
||||||
|
else:
|
||||||
raise exceptions.TagTooShort(tagname)
|
raise exceptions.TagTooShort(tagname)
|
||||||
if len(tagname) > self.config['max_tag_name_length']:
|
|
||||||
|
elif len(tagname) > self.config['max_tag_name_length']:
|
||||||
|
if warning_bag is not None:
|
||||||
|
warning_bag.add(constants.WARNING_TAG_TOO_LONG.format(tag=tagname))
|
||||||
|
else:
|
||||||
raise exceptions.TagTooLong(tagname)
|
raise exceptions.TagTooLong(tagname)
|
||||||
|
|
||||||
|
else:
|
||||||
return tagname
|
return tagname
|
||||||
|
|
||||||
class PDBUserMixin:
|
class PDBUserMixin:
|
||||||
|
@ -1200,6 +1150,8 @@ class PhotoDB(PDBAlbumMixin, PDBBookmarkMixin, PDBPhotoMixin, PDBTagMixin, PDBUs
|
||||||
*,
|
*,
|
||||||
exclude_directories=None,
|
exclude_directories=None,
|
||||||
exclude_filenames=None,
|
exclude_filenames=None,
|
||||||
|
make_albums=True,
|
||||||
|
recurse=True,
|
||||||
commit=True,
|
commit=True,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
|
@ -1220,8 +1172,11 @@ class PhotoDB(PDBAlbumMixin, PDBBookmarkMixin, PDBPhotoMixin, PDBTagMixin, PDBUs
|
||||||
directory,
|
directory,
|
||||||
exclude_directories=exclude_directories,
|
exclude_directories=exclude_directories,
|
||||||
exclude_filenames=exclude_filenames,
|
exclude_filenames=exclude_filenames,
|
||||||
|
recurse=recurse,
|
||||||
yield_style='nested',
|
yield_style='nested',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if make_albums:
|
||||||
try:
|
try:
|
||||||
album = self.get_album_by_path(directory.absolute_path)
|
album = self.get_album_by_path(directory.absolute_path)
|
||||||
except exceptions.NoSuchAlbum:
|
except exceptions.NoSuchAlbum:
|
||||||
|
@ -1230,9 +1185,18 @@ class PhotoDB(PDBAlbumMixin, PDBBookmarkMixin, PDBPhotoMixin, PDBTagMixin, PDBUs
|
||||||
commit=False,
|
commit=False,
|
||||||
title=directory.basename,
|
title=directory.basename,
|
||||||
)
|
)
|
||||||
|
|
||||||
albums = {directory.absolute_path: album}
|
albums = {directory.absolute_path: album}
|
||||||
|
|
||||||
for (current_location, directories, files) in generator:
|
for (current_location, directories, files) in generator:
|
||||||
|
for filepath in files:
|
||||||
|
try:
|
||||||
|
photo = self.new_photo(filepath.absolute_path, commit=False)
|
||||||
|
except exceptions.PhotoExists as e:
|
||||||
|
photo = e.photo
|
||||||
|
|
||||||
|
if not make_albums:
|
||||||
|
continue
|
||||||
|
|
||||||
current_album = albums.get(current_location.absolute_path, None)
|
current_album = albums.get(current_location.absolute_path, None)
|
||||||
if current_album is None:
|
if current_album is None:
|
||||||
try:
|
try:
|
||||||
|
@ -1253,17 +1217,16 @@ class PhotoDB(PDBAlbumMixin, PDBBookmarkMixin, PDBPhotoMixin, PDBTagMixin, PDBUs
|
||||||
#safeprint.safeprint('Added to %s' % parent.title)
|
#safeprint.safeprint('Added to %s' % parent.title)
|
||||||
except exceptions.GroupExists:
|
except exceptions.GroupExists:
|
||||||
pass
|
pass
|
||||||
for filepath in files:
|
|
||||||
try:
|
|
||||||
photo = self.new_photo(filepath.absolute_path, commit=False)
|
|
||||||
except exceptions.PhotoExists as e:
|
|
||||||
photo = e.photo
|
|
||||||
current_album.add_photo(photo, commit=False)
|
current_album.add_photo(photo, commit=False)
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self.log.debug('Committing - digest')
|
self.log.debug('Committing - digest')
|
||||||
self.commit()
|
self.commit()
|
||||||
|
|
||||||
|
if make_albums:
|
||||||
return album
|
return album
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
# def digest_new_files(
|
# def digest_new_files(
|
||||||
# self,
|
# self,
|
||||||
|
|
|
@ -5,19 +5,28 @@ from . import exceptions
|
||||||
from . import helpers
|
from . import helpers
|
||||||
from . import objects
|
from . import objects
|
||||||
|
|
||||||
def build_query(orderby):
|
def build_query(orderby, notnulls):
|
||||||
query = 'SELECT * FROM photos'
|
query = 'SELECT * FROM photos'
|
||||||
|
|
||||||
|
if orderby:
|
||||||
|
orderby = [o.split('-') for o in orderby]
|
||||||
|
orderby_columns = [column for (column, sorter) in orderby if column != 'RANDOM()']
|
||||||
|
else:
|
||||||
|
orderby_columns = []
|
||||||
|
|
||||||
|
if notnulls:
|
||||||
|
notnulls.extend(orderby_columns)
|
||||||
|
elif orderby_columns:
|
||||||
|
notnulls = orderby_columns
|
||||||
|
|
||||||
|
if notnulls:
|
||||||
|
notnulls = [x + ' IS NOT NULL' for x in notnulls]
|
||||||
|
notnulls = ' AND '.join(notnulls)
|
||||||
|
query += ' WHERE ' + notnulls
|
||||||
if not orderby:
|
if not orderby:
|
||||||
query += ' ORDER BY created DESC'
|
query += ' ORDER BY created DESC'
|
||||||
return query
|
return query
|
||||||
|
|
||||||
orderby = [o.split('-') for o in orderby]
|
|
||||||
whereable_columns = [column for (column, sorter) in orderby if column != 'RANDOM()']
|
|
||||||
if whereable_columns:
|
|
||||||
query += ' WHERE '
|
|
||||||
whereable_columns = [column + ' IS NOT NULL' for column in whereable_columns]
|
|
||||||
query += ' AND '.join(whereable_columns)
|
|
||||||
|
|
||||||
# Combine each column+sorter
|
# Combine each column+sorter
|
||||||
orderby = [' '.join(o) for o in orderby]
|
orderby = [' '.join(o) for o in orderby]
|
||||||
|
|
||||||
|
@ -207,7 +216,6 @@ def normalize_offset(offset, warning_bag=None):
|
||||||
|
|
||||||
return offset
|
return offset
|
||||||
|
|
||||||
|
|
||||||
def normalize_orderby(orderby, warning_bag=None):
|
def normalize_orderby(orderby, warning_bag=None):
|
||||||
if not orderby:
|
if not orderby:
|
||||||
return None
|
return None
|
||||||
|
@ -309,3 +317,28 @@ def normalize_tag_mmf(tags, photodb, warning_bag=None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return tagset
|
return tagset
|
||||||
|
|
||||||
|
def tag_expression_matcher_builder(frozen_children, warning_bag=None):
|
||||||
|
def matcher(photo_tags, tagname):
|
||||||
|
'''
|
||||||
|
Used as the `match_function` for the ExpressionTree evaluation.
|
||||||
|
|
||||||
|
photo:
|
||||||
|
The set of tag names owned by the photo in question.
|
||||||
|
tagname:
|
||||||
|
The tag which the ExpressionTree wants it to have.
|
||||||
|
'''
|
||||||
|
if not photo_tags:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
options = frozen_children[tagname]
|
||||||
|
except KeyError:
|
||||||
|
if warning_bag is not None:
|
||||||
|
warning_bag.add(constants.WARNING_NO_SUCH_TAG.format(tag=tagname))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise exceptions.NoSuchTag(tagname)
|
||||||
|
|
||||||
|
return any(option in photo_tags for option in options)
|
||||||
|
return matcher
|
||||||
|
|
|
@ -711,8 +711,10 @@ def post_edit_tags():
|
||||||
status = 400
|
status = 400
|
||||||
try:
|
try:
|
||||||
response = method(tag)
|
response = method(tag)
|
||||||
|
except exceptions.TagTooLong:
|
||||||
|
response = {'error': constants.ERROR_TAG_TOO_LONG.format(tag=tag), 'tagname': tag}
|
||||||
except exceptions.TagTooShort:
|
except exceptions.TagTooShort:
|
||||||
response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag}
|
response = {'error': constants.ERROR_TAG_TOO_SHORT.format(tag=tag), 'tagname': tag}
|
||||||
except exceptions.CantSynonymSelf:
|
except exceptions.CantSynonymSelf:
|
||||||
response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag}
|
response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag}
|
||||||
except exceptions.NoSuchTag as e:
|
except exceptions.NoSuchTag as e:
|
||||||
|
|
|
@ -88,13 +88,15 @@ li
|
||||||
display: block;
|
display: block;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.photo_card_grid
|
.photo_card_grid
|
||||||
{
|
{
|
||||||
vertical-align: middle;
|
display: inline-flex;
|
||||||
position: relative;
|
flex-direction: column;
|
||||||
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
|
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
|
||||||
display: inline-block;
|
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
@ -102,6 +104,7 @@ li
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.photo_card_grid_thumb
|
.photo_card_grid_thumb
|
||||||
{
|
{
|
||||||
|
@ -131,35 +134,32 @@ li
|
||||||
}
|
}
|
||||||
.photo_card_grid_info
|
.photo_card_grid_info
|
||||||
{
|
{
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 160px;
|
flex: 1;
|
||||||
bottom: 0px;
|
justify-content: space-between;
|
||||||
left: 8px;
|
flex-direction: column;
|
||||||
right: 8px;
|
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
.photo_card_grid_file_metadata
|
.photo_card_grid_file_metadata
|
||||||
{
|
{
|
||||||
position: absolute;
|
display: flex;
|
||||||
bottom: 0;
|
justify-content: space-between;
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
.photo_card_grid_filename
|
.photo_card_grid_filename
|
||||||
{
|
{
|
||||||
position: absolute;
|
/*position: absolute;*/
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
align-self: flex-start;
|
||||||
word-break:break-word;
|
word-break:break-word;
|
||||||
}
|
}
|
||||||
.photo_card_grid_filename:hover
|
.photo_card_grid_filename:hover
|
||||||
{
|
{
|
||||||
max-height: 100%;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
.photo_card_grid_tags
|
.photo_card_grid_tags
|
||||||
{
|
{
|
||||||
position: absolute;
|
align-self: flex-start;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
.tag_object
|
.tag_object
|
||||||
{
|
{
|
||||||
|
|
|
@ -81,6 +81,7 @@ function entry_with_history_hook(box, button)
|
||||||
box.entry_history_pos -= 1;
|
box.entry_history_pos -= 1;
|
||||||
}
|
}
|
||||||
box.value = box.entry_history[box.entry_history_pos];
|
box.value = box.entry_history[box.entry_history_pos];
|
||||||
|
setTimeout(function(){box.selectionStart = box.value.length;}, 0);
|
||||||
}
|
}
|
||||||
else if (event.keyCode == 27)
|
else if (event.keyCode == 27)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,403 +0,0 @@
|
||||||
/**
|
|
||||||
* complete.ly 1.0.0
|
|
||||||
* MIT Licensing
|
|
||||||
* Copyright (c) 2013 Lorenzo Puccetti
|
|
||||||
*
|
|
||||||
* This Software shall be used for doing good things, not bad things.
|
|
||||||
*
|
|
||||||
**/
|
|
||||||
function completely(container, config) {
|
|
||||||
config = config || {};
|
|
||||||
config.fontSize = config.fontSize || '16px';
|
|
||||||
config.fontFamily = config.fontFamily || 'sans-serif';
|
|
||||||
config.promptInnerHTML = config.promptInnerHTML || '';
|
|
||||||
config.color = config.color || '#333';
|
|
||||||
config.hintColor = config.hintColor || '#aaa';
|
|
||||||
config.backgroundColor = config.backgroundColor || '#fff';
|
|
||||||
config.dropDownBorderColor = config.dropDownBorderColor || '#aaa';
|
|
||||||
config.dropDownZIndex = config.dropDownZIndex || '100'; // to ensure we are in front of everybody
|
|
||||||
config.dropDownOnHoverBackgroundColor = config.dropDownOnHoverBackgroundColor || '#ddd';
|
|
||||||
|
|
||||||
var txtInput = document.createElement('input');
|
|
||||||
txtInput.type ='text';
|
|
||||||
txtInput.spellcheck = false;
|
|
||||||
txtInput.style.fontSize = config.fontSize;
|
|
||||||
txtInput.style.fontFamily = config.fontFamily;
|
|
||||||
txtInput.style.color = config.color;
|
|
||||||
txtInput.style.backgroundColor = config.backgroundColor;
|
|
||||||
txtInput.style.width = '100%';
|
|
||||||
txtInput.style.outline = '0';
|
|
||||||
txtInput.style.border = '0';
|
|
||||||
txtInput.style.margin = '0';
|
|
||||||
txtInput.style.padding = '0';
|
|
||||||
|
|
||||||
var txtHint = txtInput.cloneNode();
|
|
||||||
txtHint.disabled='';
|
|
||||||
txtHint.style.position = 'absolute';
|
|
||||||
txtHint.style.top = '0';
|
|
||||||
txtHint.style.left = '0';
|
|
||||||
txtHint.style.borderColor = 'transparent';
|
|
||||||
txtHint.style.boxShadow = 'none';
|
|
||||||
txtHint.style.color = config.hintColor;
|
|
||||||
|
|
||||||
txtInput.style.backgroundColor ='transparent';
|
|
||||||
txtInput.style.verticalAlign = 'top';
|
|
||||||
txtInput.style.position = 'relative';
|
|
||||||
|
|
||||||
var wrapper = document.createElement('div');
|
|
||||||
wrapper.style.position = 'relative';
|
|
||||||
wrapper.style.outline = '0';
|
|
||||||
wrapper.style.border = '0';
|
|
||||||
wrapper.style.margin = '0';
|
|
||||||
wrapper.style.padding = '0';
|
|
||||||
|
|
||||||
var prompt = document.createElement('div');
|
|
||||||
prompt.style.position = 'absolute';
|
|
||||||
prompt.style.outline = '0';
|
|
||||||
prompt.style.margin = '0';
|
|
||||||
prompt.style.padding = '0';
|
|
||||||
prompt.style.border = '0';
|
|
||||||
prompt.style.fontSize = config.fontSize;
|
|
||||||
prompt.style.fontFamily = config.fontFamily;
|
|
||||||
prompt.style.color = config.color;
|
|
||||||
prompt.style.backgroundColor = config.backgroundColor;
|
|
||||||
prompt.style.top = '0';
|
|
||||||
prompt.style.left = '0';
|
|
||||||
prompt.style.overflow = 'hidden';
|
|
||||||
prompt.innerHTML = config.promptInnerHTML;
|
|
||||||
prompt.style.background = 'transparent';
|
|
||||||
if (document.body === undefined) {
|
|
||||||
throw 'document.body is undefined. The library was wired up incorrectly.';
|
|
||||||
}
|
|
||||||
document.body.appendChild(prompt);
|
|
||||||
var w = prompt.getBoundingClientRect().right; // works out the width of the prompt.
|
|
||||||
wrapper.appendChild(prompt);
|
|
||||||
prompt.style.visibility = 'visible';
|
|
||||||
prompt.style.left = '-'+w+'px';
|
|
||||||
wrapper.style.marginLeft= w+'px';
|
|
||||||
|
|
||||||
wrapper.appendChild(txtHint);
|
|
||||||
wrapper.appendChild(txtInput);
|
|
||||||
|
|
||||||
var dropDown = document.createElement('div');
|
|
||||||
dropDown.style.position = 'absolute';
|
|
||||||
dropDown.style.visibility = 'hidden';
|
|
||||||
dropDown.style.outline = '0';
|
|
||||||
dropDown.style.margin = '0';
|
|
||||||
dropDown.style.padding = '0';
|
|
||||||
dropDown.style.textAlign = 'left';
|
|
||||||
dropDown.style.fontSize = config.fontSize;
|
|
||||||
dropDown.style.fontFamily = config.fontFamily;
|
|
||||||
dropDown.style.backgroundColor = config.backgroundColor;
|
|
||||||
dropDown.style.zIndex = config.dropDownZIndex;
|
|
||||||
dropDown.style.cursor = 'default';
|
|
||||||
dropDown.style.borderStyle = 'solid';
|
|
||||||
dropDown.style.borderWidth = '1px';
|
|
||||||
dropDown.style.borderColor = config.dropDownBorderColor;
|
|
||||||
dropDown.style.overflowX= 'hidden';
|
|
||||||
dropDown.style.whiteSpace = 'pre';
|
|
||||||
dropDown.style.overflowY = 'scroll'; // note: this might be ugly when the scrollbar is not required. however in this way the width of the dropDown takes into account
|
|
||||||
|
|
||||||
|
|
||||||
var createDropDownController = function(elem) {
|
|
||||||
var rows = [];
|
|
||||||
var ix = 0;
|
|
||||||
var oldIndex = -1;
|
|
||||||
|
|
||||||
var onMouseOver = function() { this.style.outline = '1px solid #ddd'; }
|
|
||||||
var onMouseOut = function() { this.style.outline = '0'; }
|
|
||||||
var onMouseDown = function() { p.hide(); p.onmouseselection(this.__hint); }
|
|
||||||
|
|
||||||
var p = {
|
|
||||||
hide : function() { elem.style.visibility = 'hidden'; },
|
|
||||||
refresh : function(token, array) {
|
|
||||||
elem.style.visibility = 'hidden';
|
|
||||||
ix = 0;
|
|
||||||
elem.innerHTML ='';
|
|
||||||
var vph = (window.innerHeight || document.documentElement.clientHeight);
|
|
||||||
var rect = elem.parentNode.getBoundingClientRect();
|
|
||||||
var distanceToTop = rect.top - 6; // heuristic give 6px
|
|
||||||
var distanceToBottom = vph - rect.bottom -6; // distance from the browser border.
|
|
||||||
|
|
||||||
rows = [];
|
|
||||||
for (var i=0;i<array.length;i++) {
|
|
||||||
if (array[i].indexOf(token)!==0) { continue; }
|
|
||||||
var divRow =document.createElement('div');
|
|
||||||
divRow.style.color = config.color;
|
|
||||||
divRow.onmouseover = onMouseOver;
|
|
||||||
divRow.onmouseout = onMouseOut;
|
|
||||||
divRow.onmousedown = onMouseDown;
|
|
||||||
divRow.__hint = array[i];
|
|
||||||
divRow.innerHTML = token+'<b>'+array[i].substring(token.length)+'</b>';
|
|
||||||
rows.push(divRow);
|
|
||||||
elem.appendChild(divRow);
|
|
||||||
}
|
|
||||||
if (rows.length===0) {
|
|
||||||
return; // nothing to show.
|
|
||||||
}
|
|
||||||
if (rows.length===1 && token === rows[0].__hint) {
|
|
||||||
return; // do not show the dropDown if it has only one element which matches what we have just displayed.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.length<2) return;
|
|
||||||
p.highlight(0);
|
|
||||||
|
|
||||||
if (distanceToTop > distanceToBottom*3) { // Heuristic (only when the distance to the to top is 4 times more than distance to the bottom
|
|
||||||
elem.style.maxHeight = distanceToTop+'px'; // we display the dropDown on the top of the input text
|
|
||||||
elem.style.top ='';
|
|
||||||
elem.style.bottom ='100%';
|
|
||||||
} else {
|
|
||||||
elem.style.top = '100%';
|
|
||||||
elem.style.bottom = '';
|
|
||||||
elem.style.maxHeight = distanceToBottom+'px';
|
|
||||||
}
|
|
||||||
elem.style.visibility = 'visible';
|
|
||||||
},
|
|
||||||
highlight : function(index) {
|
|
||||||
if (oldIndex !=-1 && rows[oldIndex]) {
|
|
||||||
rows[oldIndex].style.backgroundColor = config.backgroundColor;
|
|
||||||
}
|
|
||||||
rows[index].style.backgroundColor = config.dropDownOnHoverBackgroundColor; // <-- should be config
|
|
||||||
oldIndex = index;
|
|
||||||
},
|
|
||||||
move : function(step) { // moves the selection either up or down (unless it's not possible) step is either +1 or -1.
|
|
||||||
if (elem.style.visibility === 'hidden') return ''; // nothing to move if there is no dropDown. (this happens if the user hits escape and then down or up)
|
|
||||||
if (ix+step === -1 || ix+step === rows.length) return rows[ix].__hint; // NO CIRCULAR SCROLLING.
|
|
||||||
ix+=step;
|
|
||||||
p.highlight(ix);
|
|
||||||
return rows[ix].__hint;//txtShadow.value = uRows[uIndex].__hint ;
|
|
||||||
},
|
|
||||||
onmouseselection : function() {} // it will be overwritten.
|
|
||||||
};
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dropDownController = createDropDownController(dropDown);
|
|
||||||
|
|
||||||
dropDownController.onmouseselection = function(text) {
|
|
||||||
txtInput.value = txtHint.value = leftSide+text;
|
|
||||||
rs.onChange(txtInput.value); // <-- forcing it.
|
|
||||||
registerOnTextChangeOldValue = txtInput.value; // <-- ensure that mouse down will not show the dropDown now.
|
|
||||||
setTimeout(function() { txtInput.focus(); },0); // <-- I need to do this for IE
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.appendChild(dropDown);
|
|
||||||
container.appendChild(wrapper);
|
|
||||||
|
|
||||||
var spacer;
|
|
||||||
var leftSide; // <-- it will contain the leftSide part of the textfield (the bit that was already autocompleted)
|
|
||||||
|
|
||||||
|
|
||||||
function calculateWidthForText(text) {
|
|
||||||
if (spacer === undefined) { // on first call only.
|
|
||||||
spacer = document.createElement('span');
|
|
||||||
spacer.style.visibility = 'hidden';
|
|
||||||
spacer.style.position = 'fixed';
|
|
||||||
spacer.style.outline = '0';
|
|
||||||
spacer.style.margin = '0';
|
|
||||||
spacer.style.padding = '0';
|
|
||||||
spacer.style.border = '0';
|
|
||||||
spacer.style.left = '0';
|
|
||||||
spacer.style.whiteSpace = 'pre';
|
|
||||||
spacer.style.fontSize = config.fontSize;
|
|
||||||
spacer.style.fontFamily = config.fontFamily;
|
|
||||||
spacer.style.fontWeight = 'normal';
|
|
||||||
document.body.appendChild(spacer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to encode an HTML string into a plain text.
|
|
||||||
// taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding
|
|
||||||
spacer.innerHTML = String(text).replace(/&/g, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
return spacer.getBoundingClientRect().right;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var rs = {
|
|
||||||
onArrowDown : function() {}, // defaults to no action.
|
|
||||||
onArrowUp : function() {}, // defaults to no action.
|
|
||||||
onEnter : function() {}, // defaults to no action.
|
|
||||||
onTab : function() {}, // defaults to no action.
|
|
||||||
onChange: function() { rs.repaint() }, // defaults to repainting.
|
|
||||||
startFrom: 0,
|
|
||||||
options: [],
|
|
||||||
wrapper : wrapper, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
|
|
||||||
input : txtInput, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
|
|
||||||
hint : txtHint, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
|
|
||||||
dropDown : dropDown, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
|
|
||||||
prompt : prompt,
|
|
||||||
setText : function(text) {
|
|
||||||
txtHint.value = text;
|
|
||||||
txtInput.value = text;
|
|
||||||
},
|
|
||||||
getText : function() {
|
|
||||||
return txtInput.value;
|
|
||||||
},
|
|
||||||
hideDropDown : function() {
|
|
||||||
dropDownController.hide();
|
|
||||||
},
|
|
||||||
repaint : function() {
|
|
||||||
var text = txtInput.value;
|
|
||||||
var startFrom = rs.startFrom;
|
|
||||||
var options = rs.options;
|
|
||||||
var optionsLength = options.length;
|
|
||||||
|
|
||||||
// breaking text in leftSide and token.
|
|
||||||
var token = text.substring(startFrom);
|
|
||||||
leftSide = text.substring(0,startFrom);
|
|
||||||
|
|
||||||
// updating the hint.
|
|
||||||
txtHint.value ='';
|
|
||||||
for (var i=0;i<optionsLength;i++) {
|
|
||||||
var opt = options[i];
|
|
||||||
if (opt.indexOf(token)===0) { // <-- how about upperCase vs. lowercase
|
|
||||||
txtHint.value = leftSide +opt;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// moving the dropDown and refreshing it.
|
|
||||||
dropDown.style.left = calculateWidthForText(leftSide)+'px';
|
|
||||||
dropDownController.refresh(token, rs.options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var registerOnTextChangeOldValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a callback function to detect changes to the content of the input-type-text.
|
|
||||||
* Those changes are typically followed by user's action: a key-stroke event but sometimes it might be a mouse click.
|
|
||||||
**/
|
|
||||||
var registerOnTextChange = function(txt, callback) {
|
|
||||||
registerOnTextChangeOldValue = txt.value;
|
|
||||||
var handler = function() {
|
|
||||||
var value = txt.value;
|
|
||||||
if (registerOnTextChangeOldValue !== value) {
|
|
||||||
registerOnTextChangeOldValue = value;
|
|
||||||
callback(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// For user's actions, we listen to both input events and key up events
|
|
||||||
// It appears that input events are not enough so we defensively listen to key up events too.
|
|
||||||
// source: http://help.dottoro.com/ljhxklln.php
|
|
||||||
//
|
|
||||||
// The cost of listening to three sources should be negligible as the handler will invoke callback function
|
|
||||||
// only if the text.value was effectively changed.
|
|
||||||
//
|
|
||||||
//
|
|
||||||
if (txt.addEventListener) {
|
|
||||||
txt.addEventListener("input", handler, false);
|
|
||||||
txt.addEventListener('keyup', handler, false);
|
|
||||||
txt.addEventListener('change', handler, false);
|
|
||||||
} else { // is this a fair assumption: that attachEvent will exist ?
|
|
||||||
txt.attachEvent('oninput', handler); // IE<9
|
|
||||||
txt.attachEvent('onkeyup', handler); // IE<9
|
|
||||||
txt.attachEvent('onchange',handler); // IE<9
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
registerOnTextChange(txtInput,function(text) { // note the function needs to be wrapped as API-users will define their onChange
|
|
||||||
rs.onChange(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var keyDownHandler = function(e) {
|
|
||||||
e = e || window.event;
|
|
||||||
var keyCode = e.keyCode;
|
|
||||||
|
|
||||||
if (keyCode == 33) { return; } // page up (do nothing)
|
|
||||||
if (keyCode == 34) { return; } // page down (do nothing);
|
|
||||||
|
|
||||||
if (keyCode == 27) { //escape
|
|
||||||
dropDownController.hide();
|
|
||||||
txtHint.value = txtInput.value; // ensure that no hint is left.
|
|
||||||
txtInput.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == 39 || keyCode == 35 || keyCode == 9) { // right, end, tab (autocomplete triggered)
|
|
||||||
if (keyCode == 9) { // for tabs we need to ensure that we override the default behaviour: move to the next focusable HTML-element
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (txtHint.value.length == 0) {
|
|
||||||
rs.onTab(); // tab was called with no action.
|
|
||||||
// users might want to re-enable its default behaviour or handle the call somehow.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (txtHint.value.length > 0) { // if there is a hint
|
|
||||||
dropDownController.hide();
|
|
||||||
txtInput.value = txtHint.value;
|
|
||||||
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value
|
|
||||||
registerOnTextChangeOldValue = txtInput.value; // <-- to avoid dropDown to appear again.
|
|
||||||
// for example imagine the array contains the following words: bee, beef, beetroot
|
|
||||||
// user has hit enter to get 'bee' it would be prompted with the dropDown again (as beef and beetroot also match)
|
|
||||||
if (hasTextChanged) {
|
|
||||||
rs.onChange(txtInput.value); // <-- forcing it.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == 13) { // enter (autocomplete triggered)
|
|
||||||
if (txtHint.value.length == 0) { // if there is a hint
|
|
||||||
rs.onEnter();
|
|
||||||
} else {
|
|
||||||
var wasDropDownHidden = (dropDown.style.visibility == 'hidden');
|
|
||||||
dropDownController.hide();
|
|
||||||
|
|
||||||
if (wasDropDownHidden) {
|
|
||||||
txtHint.value = txtInput.value; // ensure that no hint is left.
|
|
||||||
txtInput.focus();
|
|
||||||
rs.onEnter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
txtInput.value = txtHint.value;
|
|
||||||
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value
|
|
||||||
registerOnTextChangeOldValue = txtInput.value; // <-- to avoid dropDown to appear again.
|
|
||||||
// for example imagine the array contains the following words: bee, beef, beetroot
|
|
||||||
// user has hit enter to get 'bee' it would be prompted with the dropDown again (as beef and beetroot also match)
|
|
||||||
if (hasTextChanged) {
|
|
||||||
rs.onChange(txtInput.value); // <-- forcing it.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == 40) { // down
|
|
||||||
var m = dropDownController.move(+1);
|
|
||||||
if (m == '') { rs.onArrowDown(); }
|
|
||||||
txtHint.value = leftSide+m;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyCode == 38 ) { // up
|
|
||||||
var m = dropDownController.move(-1);
|
|
||||||
if (m == '') { rs.onArrowUp(); }
|
|
||||||
txtHint.value = leftSide+m;
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's important to reset the txtHint on key down.
|
|
||||||
// think: user presses a letter (e.g. 'x') and never releases... you get (xxxxxxxxxxxxxxxxx)
|
|
||||||
// and you would see still the hint
|
|
||||||
txtHint.value =''; // resets the txtHint. (it might be updated onKeyUp)
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
if (txtInput.addEventListener) {
|
|
||||||
txtInput.addEventListener("keydown", keyDownHandler, false);
|
|
||||||
} else { // is this a fair assumption: that attachEvent will exist ?
|
|
||||||
txtInput.attachEvent('onkeydown', keyDownHandler); // IE<9
|
|
||||||
}
|
|
||||||
return rs;
|
|
||||||
}
|
|
|
@ -50,8 +50,16 @@ p
|
||||||
{% set photos = album.photos() %}
|
{% set photos = album.photos() %}
|
||||||
<span>
|
<span>
|
||||||
Download:
|
Download:
|
||||||
{% if photos %}<a href="/album/{{album.id}}.zip?recursive=no">These files</a>{% endif %}
|
{% if photos %}
|
||||||
{% if sub_albums %}<a href="/album/{{album.id}}.zip?recursive=yes">Include children</a>{% endif %}
|
<a href="/album/{{album.id}}.zip?recursive=no">
|
||||||
|
These files ({{album.sum_bytes(recurse=False, string=True)}})
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if sub_albums %}
|
||||||
|
<a href="/album/{{album.id}}.zip?recursive=yes">
|
||||||
|
Include children ({{album.sum_bytes(recurse=True, string=True)}})
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% if photos %}
|
{% if photos %}
|
||||||
<h3>Photos</h3>
|
<h3>Photos</h3>
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
align-items: baseline;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
|
||||||
}
|
}
|
||||||
.bookmark_card .bookmark_url
|
.bookmark_card .bookmark_url
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
<!DOCTYPE html5>
|
|
||||||
<html>
|
|
||||||
<style>
|
|
||||||
#left
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
#right
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
#editor_area
|
|
||||||
{
|
|
||||||
flex: 3;
|
|
||||||
}
|
|
||||||
#message_area
|
|
||||||
{
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.photo_object
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.photo_object img
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
.photo_object audio
|
|
||||||
{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.photo_object video
|
|
||||||
{
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<head>
|
|
||||||
{% import "header.html" as header %}
|
|
||||||
<title>Photo</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="stylesheet" href="/static/common.css">
|
|
||||||
<script src="/static/common.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{header.make_header()}}
|
|
||||||
<div id="content_body">
|
|
||||||
<div id="left">
|
|
||||||
<div id="editor_area">
|
|
||||||
<!-- TAG INFO -->
|
|
||||||
<h4><a href="/tags">Tags</a></h4>
|
|
||||||
<ul id="this_tags">
|
|
||||||
{% for tag in 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>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li>
|
|
||||||
<input id="add_tag_textbox" type="text" autofocus>
|
|
||||||
<button id="add_tag_button" class="add_tag_button" onclick="submit_tag(receive_callback);">add</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
{% 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>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- CONTAINING ALBUMS -->
|
|
||||||
{% set albums=photo.albums() %}
|
|
||||||
{% if 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>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="message_area">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="right">
|
|
||||||
<div class="photo_object">
|
|
||||||
{% set filename = photo.id + "." + photo.extension %}
|
|
||||||
{% set link = "/file/" + filename %}
|
|
||||||
{% set mimetype=photo.mimetype() %}
|
|
||||||
{% if mimetype == "image" %}
|
|
||||||
<img src="{{link}}">
|
|
||||||
{% elif mimetype == "video" %}
|
|
||||||
<video src="{{link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video>
|
|
||||||
{% elif mimetype == "audio" %}
|
|
||||||
<audio src="{{link}}" controls></audio>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{link}}">View {{filename}}</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
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);
|
|
||||||
|
|
||||||
function receive_callback(response)
|
|
||||||
{
|
|
||||||
var tagname = response["tagname"];
|
|
||||||
if ("error" in response)
|
|
||||||
{
|
|
||||||
message_positivity = "callback_message_negative";
|
|
||||||
message_text = '"' + tagname + '" ' + response["error"];
|
|
||||||
}
|
|
||||||
else if ("action" in response)
|
|
||||||
{
|
|
||||||
var action = response["action"];
|
|
||||||
message_positivity = "callback_message_positive";
|
|
||||||
if (action == "add_tag")
|
|
||||||
{
|
|
||||||
message_text = "Added tag " + tagname;
|
|
||||||
}
|
|
||||||
else if (action == "remove_tag")
|
|
||||||
{
|
|
||||||
message_text = "Removed tag " + tagname;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
create_message_bubble(message_positivity, message_text, 8000);
|
|
||||||
}
|
|
||||||
function submit_tag(callback)
|
|
||||||
{
|
|
||||||
add_photo_tag('{{photo.id}}', box.value, callback);
|
|
||||||
box.value='';
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -13,9 +13,10 @@
|
||||||
#content_body
|
#content_body
|
||||||
{
|
{
|
||||||
/* Override common.css */
|
/* Override common.css */
|
||||||
|
flex-direction: row;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
/*height: 100%;
|
||||||
width: 100%;
|
width: 100%;*/
|
||||||
}
|
}
|
||||||
#left
|
#left
|
||||||
{
|
{
|
||||||
|
@ -24,13 +25,41 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
#right
|
#right
|
||||||
{
|
{
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
display: flex;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 800px)
|
||||||
|
{
|
||||||
|
#content_body
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
When flexing, it tries to contain itself entirely in the screen,
|
||||||
|
forcing #left and #right to squish together.
|
||||||
|
*/
|
||||||
|
flex: none;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
#left
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Display: None will be overridden as soon as the page detects that the
|
||||||
|
screen is in narrow mode and turns off the tag box's autofocus
|
||||||
|
*/
|
||||||
|
display: none;
|
||||||
|
flex-direction: row;
|
||||||
|
width: initial;
|
||||||
|
max-width: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
#right
|
||||||
|
{
|
||||||
|
flex: none;
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#editor_area
|
#editor_area
|
||||||
{
|
{
|
||||||
|
@ -49,8 +78,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
/* height: 100%;
|
||||||
width: 100%;
|
width: 100%;*/
|
||||||
}
|
}
|
||||||
.photo_viewer a
|
.photo_viewer a
|
||||||
{
|
{
|
||||||
|
@ -67,7 +96,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
.photo_viewer img
|
#photo_img_holder img
|
||||||
{
|
{
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -146,7 +175,6 @@
|
||||||
<div class="photo_viewer">
|
<div class="photo_viewer">
|
||||||
{% if photo.mimetype == "image" %}
|
{% if photo.mimetype == "image" %}
|
||||||
<div id="photo_img_holder"><img id="photo_img" src="{{link}}" onclick="toggle_hoverzoom()" onload="this.style.opacity=0.99"></div>
|
<div id="photo_img_holder"><img id="photo_img" src="{{link}}" onclick="toggle_hoverzoom()" onload="this.style.opacity=0.99"></div>
|
||||||
<!-- <img src="{{link}}"> -->
|
|
||||||
{% elif photo.mimetype == "video" %}
|
{% elif photo.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.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video>
|
||||||
{% elif photo.mimetype == "audio" %}
|
{% elif photo.mimetype == "audio" %}
|
||||||
|
@ -161,6 +189,7 @@
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
var content_body = document.getElementById('content_body');
|
||||||
var add_tag_box = document.getElementById('add_tag_textbox');
|
var add_tag_box = document.getElementById('add_tag_textbox');
|
||||||
var add_tag_button = document.getElementById('add_tag_button');
|
var add_tag_button = document.getElementById('add_tag_button');
|
||||||
var message_area = document.getElementById('message_area');
|
var message_area = document.getElementById('message_area');
|
||||||
|
@ -239,7 +268,10 @@ function disable_hoverzoom()
|
||||||
div.style.backgroundImage = "none";
|
div.style.backgroundImage = "none";
|
||||||
div.onmousemove = null;
|
div.onmousemove = null;
|
||||||
div.onclick = null;
|
div.onclick = null;
|
||||||
|
if (getComputedStyle(content_body).flexDirection != "column-reverse")
|
||||||
|
{
|
||||||
add_tag_box.focus();
|
add_tag_box.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function toggle_hoverzoom()
|
function toggle_hoverzoom()
|
||||||
{
|
{
|
||||||
|
@ -299,5 +331,23 @@ function move_hoverzoom(event)
|
||||||
//console.log(x);
|
//console.log(x);
|
||||||
photo_img_holder.style.backgroundPosition=(-x)+"px "+(-y)+"px";
|
photo_img_holder.style.backgroundPosition=(-x)+"px "+(-y)+"px";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
/*
|
||||||
|
When the screen is in column mode, the autofocusing of the tag box snaps the
|
||||||
|
screen down to it, which is annoying. By starting the #left hidden, we have
|
||||||
|
an opportunity to unset the autofocus before showing it.
|
||||||
|
*/
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var left = document.getElementById("left");
|
||||||
|
if (getComputedStyle(content_body).flexDirection == "column-reverse")
|
||||||
|
{
|
||||||
|
add_tag_box.autofocus = false;
|
||||||
|
}
|
||||||
|
left.style.display = "flex";
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
{% if view == "list" %}
|
{% if view == "list" %}
|
||||||
<div class="photo_card_list">
|
<div class="photo_card_list">
|
||||||
<a target="_blank" href="/photo/{{photo.id}}">{{photo.basename}}</a>
|
<a target="_blank" href="/photo/{{photo.id}}">{{photo.basename}}</a>
|
||||||
|
<a target="_blank" href="/file/{{photo.id}}.{{photo.extension}}">{{photo.bytestring()}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -34,20 +35,13 @@
|
||||||
%}
|
%}
|
||||||
src="/static/basic_thumbnails/{{choice}}.png"
|
src="/static/basic_thumbnails/{{choice}}.png"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="photo_card_grid_info">
|
<div class="photo_card_grid_info">
|
||||||
<a target="_blank" class="photo_card_grid_filename" href="/photo/{{photo.id}}">{{photo.basename}}</a>
|
<a target="_blank" class="photo_card_grid_filename" href="/photo/{{photo.id}}">{{photo.basename}}</a>
|
||||||
<span class="photo_card_grid_file_metadata">
|
<div class="photo_card_grid_file_metadata">
|
||||||
{% if photo.width %}
|
<div class="photo_card_grid_tags">
|
||||||
{{photo.width}}x{{photo.height}},
|
|
||||||
{% endif %}
|
|
||||||
{% if photo.duration %}
|
|
||||||
{{photo.duration_string()}},
|
|
||||||
{% endif %}
|
|
||||||
<a target="_blank" href="/file/{{photo.id}}.{{photo.extension}}">{{photo.bytestring()}}</a>
|
|
||||||
</span>
|
|
||||||
<span class="photo_card_grid_tags">
|
|
||||||
{% set tags = photo.tags() %}
|
{% set tags = photo.tags() %}
|
||||||
{% set tag_names = [] %}
|
{% set tag_names = [] %}
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
|
@ -56,8 +50,18 @@
|
||||||
{% if tags %}
|
{% if tags %}
|
||||||
<span title="{{", ".join(tag_names)}}">T</span>
|
<span title="{{", ".join(tag_names)}}">T</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{% if photo.width %}
|
||||||
|
{{photo.width}}x{{photo.height}},
|
||||||
|
{% endif %}
|
||||||
|
{% if photo.duration %}
|
||||||
|
{{photo.duration_string()}},
|
||||||
|
{% endif %}
|
||||||
|
<a target="_blank" href="/file/{{photo.id}}.{{photo.extension}}">{{photo.bytestring()}}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
|
@ -39,7 +39,10 @@ form
|
||||||
{
|
{
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
max-width: 300px;
|
||||||
|
min-width: 300px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
#right
|
#right
|
||||||
{
|
{
|
||||||
|
@ -47,6 +50,17 @@ form
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
#content_body
|
||||||
|
{
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
#left
|
||||||
|
{
|
||||||
|
max-width: none;
|
||||||
|
width: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
.prev_next_holder
|
.prev_next_holder
|
||||||
{
|
{
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -396,7 +410,7 @@ function submit_search()
|
||||||
var expression = document.getElementsByName("tag_expression")[0].value;
|
var expression = document.getElementsByName("tag_expression")[0].value;
|
||||||
if (expression)
|
if (expression)
|
||||||
{
|
{
|
||||||
expression = expression.replace(new RegExp(" ", 'g'), "-");
|
//expression = expression.replace(new RegExp(" ", 'g'), "-");
|
||||||
parameters.push("tag_expression=" + expression);
|
parameters.push("tag_expression=" + expression);
|
||||||
has_tag_params=true;
|
has_tag_params=true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
<form method="POST">
|
|
||||||
<input type="text" name="phone_number">
|
|
||||||
<input type="submit">
|
|
||||||
</form>
|
|
Loading…
Reference in a new issue