Let PhotoTags have timestamps; use more js cards.
Tags on photos can now have timestamps, so that if you are tagging a video or audio you can reference a specific moment with your tag. In the interface, this means the tag is clickable and seeks to that point in the media. For the user interface, I am finding I need to move away from jinja for the object cards because it is too much hassle to keep the code for jinja-based cards for static rendering and the js-based cards for dynamic rendering in sync. Rather than write the same cards in two languages I can dump the JSON into the script and render the cards on load. Which makes the static HTML worse but that's what the JSON API is for anyway.
This commit is contained in:
parent
f3c8a8da3d
commit
da5c1ee008
20 changed files with 834 additions and 362 deletions
|
@ -41,7 +41,7 @@ ffmpeg = _load_ffmpeg()
|
|||
|
||||
# Database #########################################################################################
|
||||
|
||||
DATABASE_VERSION = 24
|
||||
DATABASE_VERSION = 25
|
||||
|
||||
DB_INIT = '''
|
||||
CREATE TABLE IF NOT EXISTS albums(
|
||||
|
@ -165,10 +165,12 @@ CREATE INDEX IF NOT EXISTS index_album_photo_rel_albumid on album_photo_rel(albu
|
|||
CREATE INDEX IF NOT EXISTS index_album_photo_rel_photoid on album_photo_rel(photoid);
|
||||
----------------------------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS photo_tag_rel(
|
||||
id INT PRIMARY KEY NOT NULL,
|
||||
photoid INT NOT NULL,
|
||||
tagid INT NOT NULL,
|
||||
created INT,
|
||||
PRIMARY KEY(photoid, tagid),
|
||||
timestamp REAL,
|
||||
UNIQUE(photoid, tagid, timestamp),
|
||||
FOREIGN KEY(photoid) REFERENCES photos(id),
|
||||
FOREIGN KEY(tagid) REFERENCES tags(id)
|
||||
);
|
||||
|
|
|
@ -583,7 +583,14 @@ class Album(ObjectBase, GroupableMixin):
|
|||
)
|
||||
return exists is not None
|
||||
|
||||
def jsonify(self, include_photos=True, minimal=False) -> dict:
|
||||
def jsonify(
|
||||
self,
|
||||
include_photos=True,
|
||||
include_parents=True,
|
||||
include_children=True,
|
||||
count_children=False,
|
||||
count_photos=False,
|
||||
) -> dict:
|
||||
j = {
|
||||
'type': 'album',
|
||||
'id': self.id,
|
||||
|
@ -594,12 +601,23 @@ class Album(ObjectBase, GroupableMixin):
|
|||
'thumbnail_photo': self.thumbnail_photo.id if self._thumbnail_photo else None,
|
||||
'author': self.author.jsonify() if self._author_id else None,
|
||||
}
|
||||
if not minimal:
|
||||
if self.deleted:
|
||||
j['deleted'] = True
|
||||
|
||||
if include_parents:
|
||||
j['parents'] = [parent.id for parent in self.get_parents()]
|
||||
|
||||
if include_children:
|
||||
j['children'] = [child.id for child in self.get_children()]
|
||||
|
||||
if include_photos:
|
||||
j['photos'] = [photo.id for photo in self.get_photos()]
|
||||
if include_photos:
|
||||
j['photos'] = [photo.id for photo in self.get_photos()]
|
||||
|
||||
if count_children:
|
||||
j['children_count'] = self.sum_children(recurse=False)
|
||||
|
||||
if count_photos:
|
||||
j['photos_count'] = self.sum_photos(recurse=False)
|
||||
|
||||
return j
|
||||
|
||||
|
@ -884,6 +902,9 @@ class Bookmark(ObjectBase):
|
|||
'title': self.title,
|
||||
'display_name': self.display_name,
|
||||
}
|
||||
if self.deleted:
|
||||
j['deleted'] = True
|
||||
|
||||
return j
|
||||
|
||||
class Photo(ObjectBase):
|
||||
|
@ -958,7 +979,7 @@ class Photo(ObjectBase):
|
|||
return cleaned
|
||||
|
||||
def _assign_mimetype(self):
|
||||
# This function is defined separately because it is a derivative
|
||||
# This method is defined separately because it is a derivative
|
||||
# property of the file's basename and needs to be recalculated after
|
||||
# file renames. However, I decided not to write it as a @property
|
||||
# because that would require either wasted computation or using private
|
||||
|
@ -977,23 +998,27 @@ class Photo(ObjectBase):
|
|||
def _uncache(self):
|
||||
self.photodb.caches[Photo].remove(self.id)
|
||||
|
||||
# Will add -> Tag when forward references are supported by Python.
|
||||
# Will add -> PhotoTagRel when forward references are supported by Python.
|
||||
@decorators.required_feature('photo.add_remove_tag')
|
||||
@worms.atomic
|
||||
def add_tag(self, tag):
|
||||
def add_tag(self, tag, timestamp=None):
|
||||
tag = self.photodb.get_tag(name=tag)
|
||||
|
||||
if self.has_tag(tag, check_children=False):
|
||||
return tag
|
||||
existing = self.has_tag(tag, check_children=False, match_timestamp=timestamp)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
log.info('Applying %s to %s.', tag, self)
|
||||
|
||||
data = {
|
||||
'id': self.photodb.generate_id(PhotoTagRel),
|
||||
'photoid': self.id,
|
||||
'tagid': tag.id,
|
||||
'created': timetools.now().timestamp(),
|
||||
'timestamp': PhotoTagRel.normalize_timestamp(timestamp)
|
||||
}
|
||||
self.photodb.insert(table='photo_tag_rel', pairs=data)
|
||||
self.photodb.insert(table=PhotoTagRel, pairs=data)
|
||||
photo_tag = PhotoTagRel(self.photodb, data)
|
||||
|
||||
data = {
|
||||
'id': self.id,
|
||||
|
@ -1001,7 +1026,7 @@ class Photo(ObjectBase):
|
|||
}
|
||||
self.photodb.update(table=Photo, pairs=data, where_key='id')
|
||||
|
||||
return tag
|
||||
return photo_tag
|
||||
|
||||
def atomify(self, web_root='') -> bs4.BeautifulSoup:
|
||||
web_root = web_root.rstrip('/')
|
||||
|
@ -1129,6 +1154,7 @@ class Photo(ObjectBase):
|
|||
self.set_thumbnail(image)
|
||||
return image
|
||||
|
||||
@decorators.cache_until_commit
|
||||
def get_containing_albums(self) -> set[Album]:
|
||||
'''
|
||||
Return the albums of which this photo is a member.
|
||||
|
@ -1137,7 +1163,7 @@ class Photo(ObjectBase):
|
|||
'SELECT albumid FROM album_photo_rel WHERE photoid == ?',
|
||||
[self.id]
|
||||
)
|
||||
albums = set(self.photodb.get_albums_by_id(album_ids))
|
||||
albums = frozenset(self.photodb.get_albums_by_id(album_ids))
|
||||
return albums
|
||||
|
||||
@decorators.cache_until_commit
|
||||
|
@ -1145,12 +1171,16 @@ class Photo(ObjectBase):
|
|||
'''
|
||||
Return the tags assigned to this Photo.
|
||||
'''
|
||||
tag_ids = self.photodb.select_column(
|
||||
'SELECT tagid FROM photo_tag_rel WHERE photoid == ?',
|
||||
photo_tags = frozenset(self.photodb.get_objects_by_sql(
|
||||
PhotoTagRel,
|
||||
'SELECT * FROM photo_tag_rel WHERE photoid == ?',
|
||||
[self.id]
|
||||
)
|
||||
tags = set(self.photodb.get_tags_by_id(tag_ids))
|
||||
return tags
|
||||
))
|
||||
return photo_tags
|
||||
|
||||
@decorators.cache_until_commit
|
||||
def get_tag_names(self) -> set:
|
||||
return set(photo_tag.tag.name for photo_tag in self.get_tags())
|
||||
|
||||
def get_thumbnail(self):
|
||||
query = 'SELECT thumbnail FROM photo_thumbnails WHERE photoid = ?'
|
||||
|
@ -1158,9 +1188,9 @@ class Photo(ObjectBase):
|
|||
return blob
|
||||
|
||||
# Will add -> Tag/False when forward references are supported.
|
||||
def has_tag(self, tag, *, check_children=True):
|
||||
def has_tag(self, tag, *, check_children=True, match_timestamp=False):
|
||||
'''
|
||||
Return the Tag object if this photo contains that tag.
|
||||
Return the PhotoTagRel object if this photo contains that tag.
|
||||
Otherwise return False.
|
||||
|
||||
check_children:
|
||||
|
@ -1176,15 +1206,19 @@ class Photo(ObjectBase):
|
|||
|
||||
tag_by_id = {t.id: t for t in tag_options}
|
||||
tag_option_ids = sqlhelpers.listify(tag_by_id)
|
||||
tag_id = self.photodb.select_one_value(
|
||||
f'SELECT tagid FROM photo_tag_rel WHERE photoid == ? AND tagid IN {tag_option_ids}',
|
||||
[self.id]
|
||||
)
|
||||
|
||||
if tag_id is None:
|
||||
if match_timestamp is False or match_timestamp is None:
|
||||
query = f'SELECT * FROM photo_tag_rel WHERE photoid == ? AND tagid IN {tag_option_ids}'
|
||||
bindings = [self.id]
|
||||
else:
|
||||
query = f'SELECT * FROM photo_tag_rel WHERE photoid == ? AND tagid IN {tag_option_ids} AND timestamp == ?'
|
||||
bindings = [self.id, match_timestamp]
|
||||
|
||||
results = list(self.photodb.get_objects_by_sql(PhotoTagRel, query, bindings))
|
||||
if not results:
|
||||
return False
|
||||
|
||||
return tag_by_id[tag_id]
|
||||
return results[0]
|
||||
|
||||
def has_thumbnail(self) -> bool:
|
||||
if self._has_thumbnail is not None:
|
||||
|
@ -1192,7 +1226,7 @@ class Photo(ObjectBase):
|
|||
self._has_thumbnail = self.photodb.exists('SELECT 1 FROM photo_thumbnails WHERE photoid = ?', [self.id])
|
||||
return self._has_thumbnail
|
||||
|
||||
def jsonify(self, include_albums=True, include_tags=True, minimal=False) -> dict:
|
||||
def jsonify(self, include_albums=True, include_tags=True) -> dict:
|
||||
j = {
|
||||
'type': 'photo',
|
||||
'id': self.id,
|
||||
|
@ -1213,13 +1247,14 @@ class Photo(ObjectBase):
|
|||
'searchhidden': bool(self.searchhidden),
|
||||
'simple_mimetype': self.simple_mimetype,
|
||||
}
|
||||
if self.deleted:
|
||||
j['deleted'] = True
|
||||
|
||||
if not minimal:
|
||||
if include_albums:
|
||||
j['albums'] = [album.id for album in self.get_containing_albums()]
|
||||
if include_albums:
|
||||
j['albums'] = [album.id for album in self.get_containing_albums()]
|
||||
|
||||
if include_tags:
|
||||
j['tags'] = [tag.id for tag in self.get_tags()]
|
||||
if include_tags:
|
||||
j['tags'] = [photo_tag.jsonify() for photo_tag in self.get_tags()]
|
||||
|
||||
return j
|
||||
|
||||
|
@ -1377,6 +1412,11 @@ class Photo(ObjectBase):
|
|||
@decorators.required_feature('photo.add_remove_tag')
|
||||
@worms.atomic
|
||||
def remove_tag(self, tag) -> None:
|
||||
'''
|
||||
This method removes all PhotoTagRel between this photo and tag.
|
||||
If you just want to remove one timestamped instance, get the PhotoTagRel
|
||||
object and call its delete method.
|
||||
'''
|
||||
tag = self.photodb.get_tag(name=tag)
|
||||
|
||||
log.info('Removing %s from %s.', tag, self)
|
||||
|
@ -1384,6 +1424,7 @@ class Photo(ObjectBase):
|
|||
'photoid': self.id,
|
||||
'tagid': tag.id,
|
||||
}
|
||||
|
||||
self.photodb.delete(table='photo_tag_rel', pairs=pairs)
|
||||
|
||||
data = {
|
||||
|
@ -1395,6 +1436,10 @@ class Photo(ObjectBase):
|
|||
@decorators.required_feature('photo.add_remove_tag')
|
||||
@worms.atomic
|
||||
def remove_tags(self, tags) -> None:
|
||||
'''
|
||||
This method removes all PhotoTagRel between this photo and
|
||||
multiple tags.
|
||||
'''
|
||||
tags = [self.photodb.get_tag(name=tag) for tag in tags]
|
||||
|
||||
log.info('Removing %s from %s.', tags, self)
|
||||
|
@ -1551,6 +1596,67 @@ class Photo(ObjectBase):
|
|||
self._tagged_at_dt = helpers.utcfromtimestamp(self.tagged_at_unix)
|
||||
return self._tagged_at_dt
|
||||
|
||||
class PhotoTagRel(ObjectBase):
|
||||
table = 'photo_tag_rel'
|
||||
|
||||
def __init__(self, photodb, db_row):
|
||||
super().__init__(photodb)
|
||||
self.photodb = photodb
|
||||
self.id = db_row['id']
|
||||
self.photo_id = db_row['photoid']
|
||||
self.photo = photodb.get_photo(self.photo_id)
|
||||
self.tag_id = db_row['tagid']
|
||||
self.tag = photodb.get_tag_by_id(self.tag_id)
|
||||
self.created_unix = db_row['created']
|
||||
self.timestamp = db_row['timestamp']
|
||||
|
||||
def __hash__(self):
|
||||
return hash(f'{self.photo_id}.{self.tag_id}')
|
||||
|
||||
def __lt__(self, other):
|
||||
my_tuple = (self.photo_id, self.tag.name, (self.timestamp or 0))
|
||||
other_tuple = (other.photo_id, other.tag.name, (other.timestamp or 0))
|
||||
return my_tuple < other_tuple
|
||||
|
||||
@staticmethod
|
||||
def normalize_timestamp(timestamp) -> float:
|
||||
if timestamp is None:
|
||||
return timestamp
|
||||
|
||||
if timestamp == '':
|
||||
return None
|
||||
|
||||
if isinstance(timestamp, str):
|
||||
return float(timestamp)
|
||||
|
||||
if isinstance(timestamp, (int, float)):
|
||||
return timestamp
|
||||
|
||||
else:
|
||||
raise TypeError(f'timestamp should be {float}, not {type(timestamp)}.')
|
||||
|
||||
@decorators.required_feature('photo.add_remove_tag')
|
||||
@worms.atomic
|
||||
def delete(self) -> None:
|
||||
log.info('Removing %s from %s.', self.tag.name, self.photo.id)
|
||||
self.photodb.delete(table=PhotoTagRel, pairs={'id': self.id})
|
||||
self.deleted = True
|
||||
|
||||
def jsonify(self):
|
||||
j = {
|
||||
'type': 'photo_tag_rel',
|
||||
'id': self.id,
|
||||
'photo_id': self.photo_id,
|
||||
'tag_id': self.tag_id,
|
||||
'tag_name': self.tag.name,
|
||||
'created': self.created_unix,
|
||||
'timestamp': self.timestamp,
|
||||
}
|
||||
if self.deleted:
|
||||
j['deleted'] = True
|
||||
|
||||
return j
|
||||
|
||||
class Search:
|
||||
'''
|
||||
FILE METADATA
|
||||
|
@ -1702,11 +1808,16 @@ class Search:
|
|||
results = [
|
||||
result.jsonify(include_albums=False)
|
||||
if isinstance(result, Photo) else
|
||||
result.jsonify(minimal=True)
|
||||
result.jsonify(
|
||||
include_photos=False,
|
||||
include_parents=False,
|
||||
include_children=False,
|
||||
)
|
||||
for result in self.results
|
||||
]
|
||||
|
||||
j = {
|
||||
'type': 'search',
|
||||
'kwargs': kwargs,
|
||||
'results': results,
|
||||
'more_after_limit': self.more_after_limit,
|
||||
|
@ -2060,28 +2171,6 @@ class Tag(ObjectBase, GroupableMixin):
|
|||
def _uncache(self):
|
||||
self.photodb.caches[Tag].remove(self.id)
|
||||
|
||||
def _add_child(self, member):
|
||||
ret = super()._add_child(member)
|
||||
if ret is BAIL:
|
||||
return BAIL
|
||||
|
||||
# Suppose a photo has tags A and B. Then, B is added as a child of A.
|
||||
# We should remove A from the photo leaving only the more specific B.
|
||||
# We must walk all ancestors, not just the immediate parent, because
|
||||
# the same situation could apply to a photo that has tag A, where A
|
||||
# already has children B.C.D, and then E is added as a child of D,
|
||||
# obsoleting A.
|
||||
# I expect that this method, which calls `search`, will be inefficient
|
||||
# when used in a large `add_children` loop. I would consider batching
|
||||
# up all the ancestors and doing it all at once. Just need to make sure
|
||||
# I don't cause any collateral damage e.g. removing A from a photo that
|
||||
# only has A because some other photo with A and B thinks A is obsolete.
|
||||
# This technique is nice and simple to understand for now.
|
||||
ancestors = list(member.walk_parents())
|
||||
photos = self.photodb.search(tag_musts=[member], is_searchhidden=None, yield_photos=True, yield_albums=False)
|
||||
for photo in photos.results:
|
||||
photo.remove_tags(ancestors)
|
||||
|
||||
@decorators.required_feature('tag.edit')
|
||||
@worms.atomic
|
||||
def add_child(self, member):
|
||||
|
@ -2246,21 +2335,26 @@ class Tag(ObjectBase, GroupableMixin):
|
|||
self._cached_synonyms = synonyms
|
||||
return synonyms
|
||||
|
||||
def jsonify(self, include_synonyms=False, minimal=False) -> dict:
|
||||
def jsonify(self, include_synonyms=False, include_parents=True, include_children=True) -> dict:
|
||||
j = {
|
||||
'type': 'tag',
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'created': self.created_unix,
|
||||
'author': self.author.jsonify() if self._author_id else None,
|
||||
'description': self.description,
|
||||
}
|
||||
if not minimal:
|
||||
j['author'] = self.author.jsonify() if self._author_id else None
|
||||
j['description'] = self.description
|
||||
if self.deleted:
|
||||
j['deleted'] = True
|
||||
|
||||
if include_parents:
|
||||
j['parents'] = [parent.id for parent in self.get_parents()]
|
||||
|
||||
if include_children:
|
||||
j['children'] = [child.id for child in self.get_children()]
|
||||
|
||||
if include_synonyms:
|
||||
j['synonyms'] = list(self.get_synonyms())
|
||||
if include_synonyms:
|
||||
j['synonyms'] = list(self.get_synonyms())
|
||||
|
||||
return j
|
||||
|
||||
|
@ -2532,6 +2626,9 @@ class User(ObjectBase):
|
|||
'created': self.created_unix,
|
||||
'display_name': self.display_name,
|
||||
}
|
||||
if self.deleted:
|
||||
j['deleted'] = True
|
||||
|
||||
return j
|
||||
|
||||
@decorators.required_feature('user.edit')
|
||||
|
@ -2573,4 +2670,5 @@ class WarningBag:
|
|||
self.warnings.add(warning)
|
||||
|
||||
def jsonify(self):
|
||||
return [getattr(w, 'error_message', str(w)) for w in self.warnings]
|
||||
j = [getattr(w, 'error_message', str(w)) for w in self.warnings]
|
||||
return j
|
||||
|
|
|
@ -186,7 +186,11 @@ def post_album_edit(album_id):
|
|||
album = common.P_album(album_id, response_type='json')
|
||||
album.edit(title=title, description=description)
|
||||
|
||||
response = album.jsonify(minimal=True)
|
||||
response = album.jsonify(
|
||||
include_parents=False,
|
||||
include_children=False,
|
||||
include_photos=False,
|
||||
)
|
||||
return flasktools.json_response(response)
|
||||
|
||||
@site.route('/album/<album_id>/show_in_folder', methods=['POST'])
|
||||
|
@ -257,7 +261,11 @@ def post_albums_create():
|
|||
if parent_id is not None:
|
||||
parent.add_child(album)
|
||||
|
||||
response = album.jsonify(minimal=False)
|
||||
response = album.jsonify(
|
||||
include_parents=False,
|
||||
include_children=False,
|
||||
include_photos=False,
|
||||
)
|
||||
return flasktools.json_response(response)
|
||||
|
||||
@site.route('/album/<album_id>/delete', methods=['POST'])
|
||||
|
|
|
@ -84,11 +84,6 @@ def get_thumbnail(photo_id, basename=None):
|
|||
headers=outgoing_headers,
|
||||
)
|
||||
return response
|
||||
# if photo.thumbnail:
|
||||
# path = photo.thumbnail
|
||||
# else:
|
||||
# flask.abort(404, 'That file doesnt have a thumbnail')
|
||||
# return common.send_file(path)
|
||||
|
||||
# Photo create and delete ##########################################################################
|
||||
|
||||
|
@ -104,21 +99,22 @@ def post_photo_delete(photo_id):
|
|||
|
||||
# Photo tag operations #############################################################################
|
||||
|
||||
def post_photo_add_remove_tag_core(photo_ids, tagname, add_or_remove):
|
||||
def post_photo_add_remove_tag_core(photo_ids, tagname, add_or_remove, timestamp=None):
|
||||
if isinstance(photo_ids, str):
|
||||
photo_ids = stringtools.comma_space_split(photo_ids)
|
||||
|
||||
photos = list(common.P_photos(photo_ids, response_type='json'))
|
||||
tag = common.P_tag(tagname, response_type='json')
|
||||
|
||||
response = {'action': add_or_remove, 'tagname': tag.name}
|
||||
with common.P.transaction:
|
||||
for photo in photos:
|
||||
if add_or_remove == 'add':
|
||||
photo.add_tag(tag)
|
||||
photo_tag = photo.add_tag(tag, timestamp=timestamp)
|
||||
response["photo_tag_rel_id"] = photo_tag.id
|
||||
elif add_or_remove == 'remove':
|
||||
photo.remove_tag(tag)
|
||||
|
||||
response = {'action': add_or_remove, 'tagname': tag.name}
|
||||
return flasktools.json_response(response)
|
||||
|
||||
@site.route('/photo/<photo_id>/add_tag', methods=['POST'])
|
||||
|
@ -128,12 +124,13 @@ def post_photo_add_tag(photo_id):
|
|||
Add a tag to this photo.
|
||||
'''
|
||||
common.permission_manager.basic()
|
||||
response = post_photo_add_remove_tag_core(
|
||||
photo_ids=photo_id,
|
||||
tagname=request.form['tagname'],
|
||||
add_or_remove='add',
|
||||
)
|
||||
return response
|
||||
photo = common.P_photo(photo_id, response_type='json')
|
||||
tag = common.P_tag(request.form['tagname'], response_type='json')
|
||||
|
||||
with common.P.transaction:
|
||||
tag_timestamp = request.form.get('timestamp').strip() or None
|
||||
photo_tag = photo.add_tag(tag, timestamp=tag_timestamp)
|
||||
return flasktools.json_response(photo_tag.jsonify())
|
||||
|
||||
@site.route('/photo/<photo_id>/copy_tags', methods=['POST'])
|
||||
@flasktools.required_fields(['other_photo'], forbid_whitespace=True)
|
||||
|
@ -146,7 +143,7 @@ def post_photo_copy_tags(photo_id):
|
|||
photo = common.P_photo(photo_id, response_type='json')
|
||||
other = common.P_photo(request.form['other_photo'], response_type='json')
|
||||
photo.copy_tags(other)
|
||||
return flasktools.json_response([tag.jsonify(minimal=True) for tag in photo.get_tags()])
|
||||
return flasktools.json_response([tag.jsonify() for tag in photo.get_tags()])
|
||||
|
||||
@site.route('/photo/<photo_id>/remove_tag', methods=['POST'])
|
||||
@flasktools.required_fields(['tagname'], forbid_whitespace=True)
|
||||
|
@ -162,6 +159,17 @@ def post_photo_remove_tag(photo_id):
|
|||
)
|
||||
return response
|
||||
|
||||
@site.route('/photo_tag_rel/<photo_tag_rel_id>/delete', methods=['POST'])
|
||||
def post_photo_tag_rel_delete(photo_tag_rel_id):
|
||||
'''
|
||||
Remove a tag from a photo.
|
||||
'''
|
||||
common.permission_manager.basic()
|
||||
with common.P.transaction:
|
||||
photo_tag = common.P.get_object_by_id(etiquette.objects.PhotoTagRel, photo_tag_rel_id)
|
||||
photo_tag.delete()
|
||||
return flasktools.json_response(photo_tag.jsonify())
|
||||
|
||||
@site.route('/batch/photos/add_tag', methods=['POST'])
|
||||
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
|
||||
def post_batch_photos_add_tag():
|
||||
|
@ -499,7 +507,7 @@ def get_search_html():
|
|||
total_tags = set()
|
||||
for result in search.results:
|
||||
if isinstance(result, etiquette.objects.Photo):
|
||||
total_tags.update(result.get_tags())
|
||||
total_tags.update(photo_tag.tag for photo_tag in result.get_tags())
|
||||
total_tags = sorted(total_tags, key=lambda t: t.name)
|
||||
|
||||
# PREV-NEXT PAGE URLS
|
||||
|
|
|
@ -114,7 +114,7 @@ def post_tag_remove_synonym(tagname):
|
|||
@common.permission_manager.basic_decorator
|
||||
@flasktools.cached_endpoint(max_age=15)
|
||||
def get_all_tag_names():
|
||||
all_tags = list(common.P.get_all_tag_names())
|
||||
all_tags = list(sorted(common.P.get_all_tag_names()))
|
||||
all_synonyms = common.P.get_all_synonyms()
|
||||
response = {'tags': all_tags, 'synonyms': all_synonyms}
|
||||
return flasktools.json_response(response)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
.album_card_list
|
||||
{
|
||||
display: grid;
|
||||
display: inline-grid;
|
||||
grid-template:
|
||||
"title metadata"
|
||||
/1fr;
|
||||
|
@ -122,6 +122,7 @@
|
|||
.photo_card
|
||||
{
|
||||
background-color: var(--color_secondary);
|
||||
width: max-content;
|
||||
}
|
||||
.photo_card:hover
|
||||
{
|
||||
|
@ -301,8 +302,20 @@
|
|||
/* ########################################################################## */
|
||||
/* ########################################################################## */
|
||||
|
||||
.photo_tag_card
|
||||
{
|
||||
position: relative;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* ########################################################################## */
|
||||
/* ########################################################################## */
|
||||
/* ########################################################################## */
|
||||
|
||||
.tag_card
|
||||
{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
|
@ -311,7 +324,11 @@
|
|||
color: var(--color_tag_card_fg);
|
||||
|
||||
font-size: 0.9em;
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tag_card,
|
||||
.tag_card a
|
||||
{
|
||||
color: var(--color_tag_card_fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -191,12 +191,13 @@ is hovered over.
|
|||
display: none;
|
||||
}
|
||||
|
||||
li:hover .remove_tag_button,
|
||||
.tag_card:hover ~ * .remove_tag_button,
|
||||
.tag_card:hover ~ .remove_tag_button,
|
||||
.remove_tag_button:hover,
|
||||
.remove_tag_button_perm:hover
|
||||
{
|
||||
display:inline;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#message_area
|
||||
|
|
|
@ -221,11 +221,11 @@ function edit(bookmark_id, title, b_url, callback)
|
|||
api.photos = {};
|
||||
|
||||
api.photos.add_tag =
|
||||
function add_tag(photo_id, tagname, callback)
|
||||
function add_tag(photo_id, tagname, timestamp, callback)
|
||||
{
|
||||
return http.post({
|
||||
url: `/photo/${photo_id}/add_tag`,
|
||||
data: {"tagname": tagname},
|
||||
data: {"tagname": tagname, "timestamp": timestamp},
|
||||
callback: callback,
|
||||
});
|
||||
}
|
||||
|
@ -416,6 +416,18 @@ function callback_go_to_search(response)
|
|||
window.location.href = "/search";
|
||||
}
|
||||
|
||||
/**************************************************************************************************/
|
||||
api.photo_tag_rel = {};
|
||||
|
||||
api.photo_tag_rel.delete =
|
||||
function _delete({id, callback=null})
|
||||
{
|
||||
return http.post({
|
||||
url: `/photo_tag_rel/${id}/delete`,
|
||||
callback: callback,
|
||||
});
|
||||
}
|
||||
|
||||
/**************************************************************************************************/
|
||||
api.tags = {};
|
||||
|
||||
|
|
|
@ -3,6 +3,127 @@ const cards = {};
|
|||
/******************************************************************************/
|
||||
cards.albums = {};
|
||||
|
||||
cards.albums.create =
|
||||
function create({
|
||||
album,
|
||||
view="grid",
|
||||
unlink_parent=null,
|
||||
draggable=false,
|
||||
})
|
||||
{
|
||||
const viewparam = view == "list" ? "?view=list" : "";
|
||||
|
||||
const album_card = document.createElement("div");
|
||||
album_card.classList.add("album_card");
|
||||
album_card.classList.add(`album_card_${view}`);
|
||||
album_card.dataset.id = album == "root" ? "root" : album.id;
|
||||
album_card.ondragstart = cards.albums.drag_start
|
||||
album_card.ondragend = cards.albums.drag_end
|
||||
album_card.ondragover = cards.albums.drag_over
|
||||
album_card.ondrop = cards.albums.drag_drop
|
||||
album_card.draggable = draggable && album != "root";
|
||||
|
||||
const thumbnail_link = document.createElement("a");
|
||||
thumbnail_link.classList.add("album_card_thumbnail");
|
||||
thumbnail_link.draggable = false;
|
||||
album_card.append(thumbnail_link);
|
||||
|
||||
const thumbnail_img = document.createElement("img");
|
||||
thumbnail_img.loading = "lazy";
|
||||
thumbnail_img.draggable = false;
|
||||
thumbnail_link.append(thumbnail_img);
|
||||
|
||||
let href;
|
||||
if (album == "root")
|
||||
{
|
||||
href = `/albums${viewparam}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
href = `/album/${album.id}${viewparam}`
|
||||
}
|
||||
thumbnail_link.href = href;
|
||||
|
||||
if (album == "root")
|
||||
{
|
||||
thumbnail_img.src = "/static/basic_thumbnails/album.png";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (album.thumbnail_photo)
|
||||
{
|
||||
thumbnail_img.src = `/photo/${album.thumbnail_photo}/thumbnail/${album.thumbnail_photo.id}.jpg`;
|
||||
}
|
||||
else
|
||||
{
|
||||
thumbnail_img.src = "/static/basic_thumbnails/album.png";
|
||||
}
|
||||
}
|
||||
|
||||
const album_title = document.createElement("a");
|
||||
album_card.append(album_title);
|
||||
album_title.classList.add("album_card_title");
|
||||
album_title.draggable = false;
|
||||
album_title.href = href;
|
||||
if (album == "root")
|
||||
{
|
||||
album_title.innerText = "Albums";
|
||||
}
|
||||
else
|
||||
{
|
||||
album_title.innerText = album.display_name;
|
||||
}
|
||||
|
||||
const album_metadata = document.createElement("div");
|
||||
album_metadata.classList.add("album_card_metadata");
|
||||
album_card.append(album_metadata);
|
||||
if (album != "root")
|
||||
{
|
||||
const child_count = document.createElement("span");
|
||||
child_count.classList.add("album_card_child_count");
|
||||
child_count.title = `${album.children_count} child albums`;
|
||||
child_count.innerText = album.children_count;
|
||||
album_metadata.append(child_count);
|
||||
|
||||
album_metadata.append(" | ");
|
||||
|
||||
const photo_count = document.createElement("span");
|
||||
photo_count.classList.add("album_card_photo_count");
|
||||
photo_count.title = `${album.photos_count} photos`;
|
||||
photo_count.innerText = album.photos_count;
|
||||
album_metadata.append(photo_count);
|
||||
}
|
||||
|
||||
const album_tools = document.createElement("div");
|
||||
album_tools.classList.add("album_card_tools");
|
||||
album_card.append(album_tools);
|
||||
|
||||
if (unlink_parent !== null)
|
||||
{
|
||||
const unlink_button = document.createElement("button");
|
||||
unlink_button.classList.add("remove_child_button");
|
||||
unlink_button.classList.add("red_button");
|
||||
|
||||
unlink_button.classList.add("button_with_confirm");
|
||||
unlink_button.dataset.prompt = "Remove child?";
|
||||
unlink_button.dataset.holderClass = "remove_child_button";
|
||||
unlink_button.dataset.confirmClass = "red_button";
|
||||
unlink_button.dataset.cancelClass = "gray_button";
|
||||
unlink_button.innerText = "Unlink";
|
||||
|
||||
unlink_button.addEventListener("click", function(event)
|
||||
{
|
||||
return api.albums.remove_child(
|
||||
unlink_parent,
|
||||
album.id,
|
||||
common.refresh_or_alert
|
||||
);
|
||||
});
|
||||
album_tools.append(unlink_button);
|
||||
}
|
||||
return album_card;
|
||||
}
|
||||
|
||||
cards.albums.drag_start =
|
||||
function drag_start(event)
|
||||
{
|
||||
|
@ -72,7 +193,12 @@ function drag_drop(event)
|
|||
cards.bookmarks = {};
|
||||
|
||||
cards.bookmarks.create =
|
||||
function create(bookmark, add_author, add_delete_button, add_url_element)
|
||||
function create({
|
||||
bookmark,
|
||||
add_author=false,
|
||||
add_delete_button=false,
|
||||
add_url_element=false,
|
||||
})
|
||||
{
|
||||
const bookmark_card = document.createElement("div");
|
||||
bookmark_card.className = "bookmark_card"
|
||||
|
@ -119,6 +245,16 @@ function create(bookmark, add_author, add_delete_button, add_url_element)
|
|||
}
|
||||
}
|
||||
|
||||
if (add_author && bookmark.author !== null)
|
||||
{
|
||||
const p = document.createElement("p");
|
||||
const authorlink = document.createElement("a");
|
||||
authorlink.href = "/userid/" + bookmark.author.id;
|
||||
authorlink.innerText = bookmark.author.display_name;
|
||||
p.append(authorlink);
|
||||
bookmark_card.append(p);
|
||||
}
|
||||
|
||||
return bookmark_card;
|
||||
}
|
||||
|
||||
|
@ -150,7 +286,7 @@ function file_link(photo, short)
|
|||
}
|
||||
|
||||
cards.photos.create =
|
||||
function create(photo, view)
|
||||
function create({photo, view="grid"})
|
||||
{
|
||||
if (view !== "list" && view !== "grid")
|
||||
{
|
||||
|
@ -230,16 +366,16 @@ function create(photo, view)
|
|||
photo_card.appendChild(photo_card_thumbnail);
|
||||
}
|
||||
|
||||
let tag_names_title = [];
|
||||
let tag_names_title = new Set();
|
||||
let tag_names_inner = "";
|
||||
for (const tag of photo.tags)
|
||||
for (const photo_tag of photo.tags)
|
||||
{
|
||||
tag_names_title.push(tag.name);
|
||||
tag_names_title.add(photo_tag.tag_name);
|
||||
tag_names_inner = "T";
|
||||
}
|
||||
const photo_card_tags = document.createElement("span");
|
||||
photo_card_tags.className = "photo_card_tags";
|
||||
photo_card_tags.title = tag_names_title.join(",");
|
||||
photo_card_tags.title = Array.from(tag_names_title).join(",");
|
||||
photo_card_tags.innerText = tag_names_inner;
|
||||
photo_card.appendChild(photo_card_tags);
|
||||
|
||||
|
@ -320,5 +456,74 @@ function photo_rightclick(event)
|
|||
return false;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
cards.photo_tags = {};
|
||||
|
||||
cards.photo_tags.create =
|
||||
function create({photo_tag, timestamp_onclick=null, remove_button_onclick=null})
|
||||
{
|
||||
const photo_tag_card = document.createElement("div");
|
||||
console.log(photo_tag);
|
||||
photo_tag_card.dataset.id = photo_tag.id;
|
||||
photo_tag_card.classList.add("photo_tag_card");
|
||||
|
||||
const tag = {"id": photo_tag.tag_id, "name": photo_tag.tag_name};
|
||||
const tag_card = cards.tags.create({tag: tag});
|
||||
photo_tag_card.append(tag_card);
|
||||
|
||||
if (photo_tag.timestamp !== null)
|
||||
{
|
||||
const timestamp = document.createElement("a");
|
||||
timestamp.innerText = " " + common.seconds_to_hms({seconds: photo_tag.timestamp});
|
||||
timestamp.addEventListener("click", timestamp_onclick);
|
||||
photo_tag_card.append(timestamp)
|
||||
}
|
||||
if (remove_button_onclick !== null)
|
||||
{
|
||||
const remove_button = document.createElement("button");
|
||||
remove_button.classList.add("remove_tag_button");
|
||||
remove_button.classList.add("red_button");
|
||||
remove_button.addEventListener("click", remove_button_onclick);
|
||||
photo_tag_card.append(remove_button);
|
||||
}
|
||||
return photo_tag_card;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
cards.tags = {};
|
||||
|
||||
cards.tags.create =
|
||||
function create({tag, extra_classes=[], link="info", innertext=null, add_alt_description=false})
|
||||
{
|
||||
const tag_card = document.createElement("div");
|
||||
tag_card.dataset.id = tag.id;
|
||||
tag_card.classList.add("tag_card");
|
||||
for (const cls of extra_classes)
|
||||
{
|
||||
tag_card.classList.add(cls);
|
||||
}
|
||||
|
||||
const a_or_span = link === null ? "span" : "a";
|
||||
const tag_text = document.createElement(a_or_span);
|
||||
tag_text.innerText = innertext || tag.name;
|
||||
if (add_alt_description && tag.description != "")
|
||||
{
|
||||
tag_text.title = tag.description;
|
||||
}
|
||||
tag_card.append(tag_text);
|
||||
|
||||
const href_options = {
|
||||
"search": `/search?tag_musts=${encodeURIComponent(tag.name)}`,
|
||||
"search_musts":`/search?tag_musts=${encodeURIComponent(tag.name)}`,
|
||||
"search_mays": `/search?tag_mays=${encodeURIComponent(tag.name)}`,
|
||||
"search_forbids": `/search?tag_forbids=${encodeURIComponent(tag.name)}`,
|
||||
"info": `/tag/${encodeURIComponent(tag.name)}`,
|
||||
};
|
||||
const href = href_options[link] || null;
|
||||
if (href !== null)
|
||||
{
|
||||
tag_text.href = href;
|
||||
}
|
||||
|
||||
return tag_card;
|
||||
}
|
||||
|
|
|
@ -88,13 +88,13 @@ function hms_render_colons(hours, minutes, seconds)
|
|||
}
|
||||
|
||||
common.seconds_to_hms =
|
||||
function seconds_to_hms(seconds, args)
|
||||
function seconds_to_hms({
|
||||
seconds,
|
||||
renderer=common.hms_render_colons,
|
||||
force_minutes=false,
|
||||
force_hours=false,
|
||||
})
|
||||
{
|
||||
args = args || {};
|
||||
const renderer = args["renderer"] || common.hms_render_colons;
|
||||
const force_minutes = args["force_minutes"] || false;
|
||||
const force_hours = args["force_hours"] || false;
|
||||
|
||||
if (seconds > 0 && seconds < 1)
|
||||
{
|
||||
seconds = 1;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<script src="/static/js/editor.js"></script>
|
||||
|
||||
<style>
|
||||
#bookmark_panel,
|
||||
#bookmark_list
|
||||
{
|
||||
display: flex;
|
||||
|
@ -35,11 +36,10 @@
|
|||
<body>
|
||||
{{header.make_header(session=request.session)}}
|
||||
<div id="content_body">
|
||||
<div id="bookmark_list" class="panel">
|
||||
<h1>{{bookmarks|length}} Bookmarks</h1>
|
||||
{% for bookmark in bookmarks %}
|
||||
{{cards.create_bookmark_card(bookmark, add_delete_button=True, add_url_element=True)}}
|
||||
{% endfor %}
|
||||
<div id="bookmark_panel" class="panel">
|
||||
<h1><span class="dynamic_bookmark_count">{{bookmarks|length}}</span> Bookmarks</h1>
|
||||
<div id="bookmark_list">
|
||||
</div>
|
||||
|
||||
<div id="new_bookmark_card" class="bookmark_card">
|
||||
<input id="new_bookmark_title" type="text" placeholder="title (optional)">
|
||||
|
@ -53,6 +53,12 @@
|
|||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
const BOOKMARKS = [
|
||||
{% for bookmark in bookmarks %}
|
||||
{{bookmark.jsonify()|tojson|safe}},
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
function create_bookmark_form()
|
||||
{
|
||||
const url = document.getElementById("new_bookmark_url").value.trim();
|
||||
|
@ -72,20 +78,17 @@ function create_bookmark_callback(response)
|
|||
return;
|
||||
}
|
||||
const bookmark = response.data;
|
||||
const add_author = true;
|
||||
const add_delete_button = true;
|
||||
const add_url_element = true;
|
||||
const bookmark_card = cards.bookmarks.create(
|
||||
bookmark,
|
||||
add_author,
|
||||
add_delete_button,
|
||||
add_url_element
|
||||
);
|
||||
const bookmark_card = cards.bookmarks.create({
|
||||
bookmark: bookmark,
|
||||
add_author: true,
|
||||
add_delete_button: true,
|
||||
add_url_element: true,
|
||||
});
|
||||
create_editor(bookmark_card);
|
||||
|
||||
const bookmark_list = document.getElementById("bookmark_list");
|
||||
const bookmark_panel = document.getElementById("bookmark_panel");
|
||||
const new_bookmark_card = document.getElementById("new_bookmark_card");
|
||||
bookmark_list.insertBefore(bookmark_card, new_bookmark_card);
|
||||
bookmark_panel.insertBefore(bookmark_card, new_bookmark_card);
|
||||
|
||||
document.getElementById("new_bookmark_url").value = "";
|
||||
document.getElementById("new_bookmark_title").value = "";
|
||||
|
@ -182,6 +185,17 @@ function create_editors()
|
|||
|
||||
function on_pageload()
|
||||
{
|
||||
common.update_dynamic_elements("dynamic_bookmark_count", BOOKMARKS.length);
|
||||
for (const bookmark of BOOKMARKS)
|
||||
{
|
||||
const bookmark_card = cards.bookmarks.create({
|
||||
bookmark: bookmark,
|
||||
add_author: true,
|
||||
add_delete_button: true,
|
||||
add_url_element: true,
|
||||
});
|
||||
document.getElementById("bookmark_list").append(bookmark_card);
|
||||
}
|
||||
create_editors();
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", on_pageload);
|
||||
|
|
|
@ -63,46 +63,6 @@ draggable=true
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# BOOKMARK ###################################################################}
|
||||
|
||||
{% macro create_bookmark_card(bookmark, add_author=True, add_delete_button=False, add_url_element=False) %}
|
||||
<div class="bookmark_card" data-id="{{bookmark.id}}">
|
||||
<h2><a href="{{bookmark.url}}" class="bookmark_title" title="{{bookmark.url}}">{{bookmark.display_name}}</a></h2>
|
||||
|
||||
{# The URL element is always display:none, but its presence is useful in #}
|
||||
{# facilitating the Editor object. If this bookmark will not be editable, #}
|
||||
{# there is no need for it. #}
|
||||
{% if add_url_element %}
|
||||
<a href="{{bookmark.url}}" class="bookmark_url">{{bookmark.url}}</a>
|
||||
{% endif %}
|
||||
|
||||
{# if more tools are added, this will become an `or` #}
|
||||
{% if add_delete_button %}
|
||||
<div class="bookmark_toolbox">
|
||||
{% if add_delete_button %}
|
||||
<button
|
||||
class="red_button button_with_confirm"
|
||||
data-onclick="return delete_bookmark_form(event);"
|
||||
data-prompt="Delete Bookmark?"
|
||||
data-cancel-class="gray_button"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if add_author %}
|
||||
{% set author = bookmark.author %}
|
||||
{% if author is not none %}
|
||||
<p>
|
||||
Author: <a href="/userid/{{author.id}}">{{author.display_name}}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# PHOTO ######################################################################}
|
||||
|
||||
{# Priority: specific extensions > specific mimetypes > general mimtypes #}
|
||||
|
@ -132,11 +92,7 @@ draggable=true
|
|||
{% do metadatas.append("{d}".format(d=photo.duration_string)) %}
|
||||
{% endif -%}
|
||||
|
||||
{% set tag_names_title = [] %}
|
||||
{% for tag in photo.get_tags() %}
|
||||
{% do tag_names_title.append(tag.name) %}
|
||||
{% endfor -%}
|
||||
{% set tag_names_title = tag_names_title|comma_join %}
|
||||
{% set tag_names_title = photo.get_tag_names()|sort|comma_join %}
|
||||
{% set tag_names_inner = "T" if tag_names_title else "" %}
|
||||
|
||||
<div
|
||||
|
@ -203,6 +159,7 @@ draggable="true"
|
|||
-->
|
||||
{%- macro create_tag_card(
|
||||
tag,
|
||||
photo_tag_rel_id=None,
|
||||
extra_classes="",
|
||||
innertext=None,
|
||||
innertext_safe=None,
|
||||
|
@ -225,6 +182,6 @@ draggable="true"
|
|||
{%- set innertext = innertext_safe or (innertext or tag.name)|e -%}
|
||||
{%- set element = "a" if (link or onclick) else "span" -%}
|
||||
|
||||
<{{element}} {{make_attributes(class=class, title=title, href=href, onclick=onclick, **kwargs)|safe}}>{{innertext|safe}}</{{element}}>
|
||||
<{{element}} {{make_attributes(data_id=photo_tag_rel_id, class=class, title=title, href=href, onclick=onclick, **kwargs)|safe}}>{{innertext|safe}}</{{element}}>
|
||||
{{-''-}}
|
||||
{%- endmacro -%}
|
||||
|
|
|
@ -233,7 +233,7 @@ function request_more_divs()
|
|||
}
|
||||
for (const photo of response.data)
|
||||
{
|
||||
photo_div = cards.photos.create(photo);
|
||||
photo_div = cards.photos.create({photo: photo});
|
||||
divs[photo.id] = photo_div;
|
||||
needed.delete(photo.id)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<link rel="stylesheet" href="/static/css/cards.css">
|
||||
<script src="/static/js/common.js"></script>
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/cards.js"></script>
|
||||
<script src="/static/js/hotkeys.js"></script>
|
||||
<script src="/static/js/http.js"></script>
|
||||
<script src="/static/js/photo_clipboard.js"></script>
|
||||
|
@ -165,21 +166,14 @@
|
|||
|
||||
<!-- TAG INFO -->
|
||||
<h4>Tags</h4>
|
||||
{% if photo.simple_mimetype == "audio" or photo.simple_mimetype == "video" %}
|
||||
<label><input type="checkbox" id="use_photo_tag_timestamps"/>Add tags with timestamps</label>
|
||||
{% endif %}
|
||||
<ul id="this_tags">
|
||||
<li>
|
||||
<li id="add_tag_li">
|
||||
<input type="text" id="add_tag_textbox" class="entry_with_history entry_with_tagname_replacements" list="tag_autocomplete_datalist">
|
||||
<button id="add_tag_button" class="green_button" onclick="return add_photo_tag_form();">add</button>
|
||||
</li>
|
||||
{% set tags = photo.get_tags()|sort(attribute='name') %}
|
||||
{% for tag in tags %}
|
||||
<li>
|
||||
{{cards.create_tag_card(tag, link="info", with_alt_description=True)}}<!--
|
||||
--><button
|
||||
class="remove_tag_button red_button"
|
||||
onclick="return remove_photo_tag_form('{{photo.id}}', '{{tag.name}}');">
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<!-- METADATA & DOWNLOAD -->
|
||||
|
@ -313,12 +307,23 @@
|
|||
<script type="text/javascript">
|
||||
const PHOTO_ID = "{{photo.id}}";
|
||||
|
||||
const PHOTO_TAGS = [
|
||||
{% for photo_tag in photo.get_tags()|sort %}
|
||||
{{photo_tag.jsonify()|tojson|safe}},
|
||||
{% endfor %}
|
||||
];
|
||||
const add_tag_box = document.getElementById('add_tag_textbox');
|
||||
const add_tag_button = document.getElementById('add_tag_button');
|
||||
common.bind_box_to_button(add_tag_box, add_tag_button, false);
|
||||
|
||||
const message_area = document.getElementById('message_area');
|
||||
|
||||
const PHOTO_MEDIA = (
|
||||
document.querySelector(".photo_viewer_video video")
|
||||
|| document.querySelector(".photo_viewer_audio audio")
|
||||
|| null
|
||||
);
|
||||
|
||||
// API /////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function add_photo_tag_form()
|
||||
|
@ -328,7 +333,8 @@ function add_photo_tag_form()
|
|||
{
|
||||
return;
|
||||
}
|
||||
api.photos.add_tag(PHOTO_ID, tagname, add_photo_tag_callback);
|
||||
const timestamp = get_media_timestamp();
|
||||
api.photos.add_tag(PHOTO_ID, tagname, timestamp, add_photo_tag_callback);
|
||||
add_tag_box.value = "";
|
||||
}
|
||||
|
||||
|
@ -339,27 +345,29 @@ function add_photo_tag_callback(response)
|
|||
{
|
||||
return;
|
||||
}
|
||||
const photo_tag = response.data;
|
||||
|
||||
const this_tags = document.getElementById("this_tags");
|
||||
const tag_cards = this_tags.getElementsByClassName("tag_card");
|
||||
for (const tag_card of tag_cards)
|
||||
const photo_tag_cards = this_tags.getElementsByClassName("photo_tag_card");
|
||||
for (const photo_tag_card of photo_tag_cards)
|
||||
{
|
||||
if (tag_card.innerText === response.data.tagname)
|
||||
if (photo_tag_card.dataset.id == photo_tag.id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const li = document.createElement("li");
|
||||
const tag_card = document.createElement("a");
|
||||
tag_card.className = "tag_card"
|
||||
tag_card.href = "/tag/" + response.data.tagname;
|
||||
tag_card.innerText = response.data.tagname;
|
||||
const remove_button = document.createElement("button");
|
||||
remove_button.className = "remove_tag_button red_button"
|
||||
remove_button.onclick = () => remove_photo_tag_form(PHOTO_ID, response.data.tagname);
|
||||
li.appendChild(tag_card);
|
||||
li.appendChild(remove_button);
|
||||
this_tags.appendChild(li);
|
||||
const card = cards.photo_tags.create({
|
||||
photo_tag: photo_tag,
|
||||
"timestamp_onclick": ()=>{seek_media(photo_tag.timestamp)},
|
||||
"remove_button_onclick": ()=>{remove_photo_tag_form(photo_tag.id)},
|
||||
});
|
||||
li.append(card);
|
||||
this_tags.append(li);
|
||||
|
||||
sort_tag_cards();
|
||||
common.create_message_bubble(message_area, "message_positive", `Added tag ${response.data.tag_name}`, 8000);
|
||||
}
|
||||
|
||||
function copy_other_photo_tags_form(event)
|
||||
|
@ -372,9 +380,9 @@ function copy_other_photo_tags_form(event)
|
|||
api.photos.copy_tags(PHOTO_ID, other_photo, common.refresh_or_alert);
|
||||
}
|
||||
|
||||
function remove_photo_tag_form(photo_id, tagname)
|
||||
function remove_photo_tag_form(photo_tag_rel_id)
|
||||
{
|
||||
api.photos.remove_tag(photo_id, tagname, remove_photo_tag_callback);
|
||||
api.photo_tag_rel.delete({id: photo_tag_rel_id, callback: remove_photo_tag_callback});
|
||||
add_tag_box.focus();
|
||||
}
|
||||
|
||||
|
@ -385,15 +393,17 @@ function remove_photo_tag_callback(response)
|
|||
{
|
||||
return;
|
||||
}
|
||||
const tag_cards = document.getElementById("this_tags").getElementsByClassName("tag_card");
|
||||
for (const tag_card of tag_cards)
|
||||
const photo_tag_cards = document.getElementById("this_tags").getElementsByClassName("photo_tag_card");
|
||||
for (const photo_tag_card of photo_tag_cards)
|
||||
{
|
||||
if (tag_card.innerText === response.data.tagname)
|
||||
console.log(photo_tag_card);
|
||||
if (photo_tag_card.dataset.id == response.data.id)
|
||||
{
|
||||
const li = tag_card.parentElement;
|
||||
const li = photo_tag_card.parentElement;
|
||||
li.parentElement.removeChild(li);
|
||||
}
|
||||
}
|
||||
common.create_message_bubble(message_area, "message_positive", `Removed tag ${response.data.tag_name}`, 8000);
|
||||
}
|
||||
|
||||
function add_remove_photo_tag_callback(response)
|
||||
|
@ -403,30 +413,12 @@ function add_remove_photo_tag_callback(response)
|
|||
alert(JSON.stringify(response));
|
||||
return;
|
||||
}
|
||||
let message_text;
|
||||
let message_positivity;
|
||||
let abort;
|
||||
let abort = false;
|
||||
if ("error_type" in response.data)
|
||||
{
|
||||
message_positivity = "message_negative";
|
||||
message_text = response.data.error_message;
|
||||
abort = true;
|
||||
common.create_message_bubble(message_area, "message_negative", response.data.error_message, 8000);
|
||||
}
|
||||
else
|
||||
{
|
||||
const tagname = response.data.tagname;
|
||||
message_positivity = "message_positive";
|
||||
if (response.meta.kwargs.url.includes("add_tag"))
|
||||
{
|
||||
message_text = "Added tag " + tagname;
|
||||
}
|
||||
else if (response.meta.kwargs.url.includes("remove_tag"))
|
||||
{
|
||||
message_text = "Removed tag " + tagname;
|
||||
}
|
||||
abort = false;
|
||||
}
|
||||
common.create_message_bubble(message_area, message_positivity, message_text, 8000);
|
||||