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,11 +910,19 @@ 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']: | ||||||
|             raise exceptions.TagTooShort(tagname) |             if warning_bag is not None: | ||||||
|         if len(tagname) > self.config['max_tag_name_length']: |                 warning_bag.add(constants.WARNING_TAG_TOO_SHORT.format(tag=tagname)) | ||||||
|             raise exceptions.TagTooLong(tagname) |             else: | ||||||
|  |                 raise exceptions.TagTooShort(tagname) | ||||||
| 
 | 
 | ||||||
|         return tagname |         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) | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             return tagname | ||||||
| 
 | 
 | ||||||
| class PDBUserMixin: | class PDBUserMixin: | ||||||
|     def generate_user_id(self): |     def generate_user_id(self): | ||||||
|  | @ -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,19 +1172,31 @@ 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', | ||||||
|         ) |         ) | ||||||
|         try: |  | ||||||
|             album = self.get_album_by_path(directory.absolute_path) |  | ||||||
|         except exceptions.NoSuchAlbum: |  | ||||||
|             album = self.new_album( |  | ||||||
|                 associated_directory=directory.absolute_path, |  | ||||||
|                 commit=False, |  | ||||||
|                 title=directory.basename, |  | ||||||
|             ) |  | ||||||
| 
 | 
 | ||||||
|         albums = {directory.absolute_path: album} |         if make_albums: | ||||||
|  |             try: | ||||||
|  |                 album = self.get_album_by_path(directory.absolute_path) | ||||||
|  |             except exceptions.NoSuchAlbum: | ||||||
|  |                 album = self.new_album( | ||||||
|  |                     associated_directory=directory.absolute_path, | ||||||
|  |                     commit=False, | ||||||
|  |                     title=directory.basename, | ||||||
|  |                 ) | ||||||
|  |             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() | ||||||
|         return album | 
 | ||||||
|  |         if make_albums: | ||||||
|  |             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; | ||||||
|     add_tag_box.focus(); |     if (getComputedStyle(content_body).flexDirection != "column-reverse") | ||||||
|  |     { | ||||||
|  |         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,29 +35,32 @@ | ||||||
|                 %} |                 %} | ||||||
|                 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}}, |                 {% set tags = photo.tags() %} | ||||||
|         {% endif %} |                 {% set tag_names = [] %} | ||||||
|         {% if photo.duration %} |                 {% for tag in tags %} | ||||||
|             {{photo.duration_string()}}, |                 {% do tag_names.append(tag.name) %} | ||||||
|         {% endif %} |                 {% endfor %} | ||||||
|         <a target="_blank" href="/file/{{photo.id}}.{{photo.extension}}">{{photo.bytestring()}}</a> |                 {% if tags %} | ||||||
|         </span> |                     <span title="{{", ".join(tag_names)}}">T</span> | ||||||
|         <span class="photo_card_grid_tags"> |                 {% endif %} | ||||||
|             {% set tags = photo.tags() %} |             </div> | ||||||
|             {% set tag_names = [] %} |             <span> | ||||||
|             {% for tag in tags %} |             {% if photo.width %} | ||||||
|             {% do tag_names.append(tag.name) %} |                 {{photo.width}}x{{photo.height}}, | ||||||
|             {% endfor %} |  | ||||||
|             {% if tags %} |  | ||||||
|                 <span title="{{", ".join(tag_names)}}">T</span> |  | ||||||
|             {% endif %} |             {% endif %} | ||||||
|         </span> |             {% if photo.duration %} | ||||||
|  |                 {{photo.duration_string()}}, | ||||||
|  |             {% endif %} | ||||||
|  |             <a target="_blank" href="/file/{{photo.id}}.{{photo.extension}}">{{photo.bytestring()}}</a> | ||||||
|  |             </span> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|  | @ -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