Add context menu script

This commit is contained in:
Michał 2023-09-24 19:53:14 +01:00
parent 2db4d0a6ea
commit 8a4fe891ef
9 changed files with 463 additions and 96 deletions

View file

@ -7,6 +7,9 @@ import re
import logging import logging
from uuid import uuid4 from uuid import uuid4
from PIL import Image
from PIL.ExifTags import TAGS
from flask import ( from flask import (
Blueprint, Blueprint,
abort, abort,
@ -20,8 +23,7 @@ from flask_login import login_required, current_user
from colorthief import ColorThief from colorthief import ColorThief
from onlylegs.extensions import db from onlylegs.extensions import db
from onlylegs.models import Users, Pictures from onlylegs.models import Users, Pictures, Exif
from onlylegs.utils.metadata import yoink
from onlylegs.utils.generate_image import generate_thumbnail from onlylegs.utils.generate_image import generate_thumbnail
@ -137,46 +139,91 @@ def upload():
""" """
Uploads an image to the server and saves it to the database Uploads an image to the server and saves it to the database
""" """
form_file = request.files.get("file", None) image_description = request.form.get("description", "").strip()
form = request.form image_alt = request.form.get("alt", "").strip()
image_file = request.files.get("file", None)
if not form_file: if not image_file:
return jsonify({"message": "No file"}), 400 return jsonify({"message": "No file"}), 400
# Get file extension, generate random name and set file path # Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower() image_mime = pathlib.Path(image_file.filename).suffix.replace(".", "").lower()
img_name = "GWAGWA_" + str(uuid4()) image_name = "GWAGWA_" + str(uuid4())
img_path = os.path.join( image_filename = image_name + "." + image_mime
current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext save_path = os.path.join(current_app.config["UPLOAD_FOLDER"], image_filename)
)
# Check if file extension is allowed if image_mime not in current_app.config["ALLOWED_EXTENSIONS"].keys():
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): logging.info("File extension not allowed: %s", image_mime)
logging.info("File extension not allowed: %s", img_ext)
return jsonify({"message": "File extension not allowed"}), 403 return jsonify({"message": "File extension not allowed"}), 403
# Save file image_file.save(save_path)
try:
form_file.save(img_path)
except OSError as err:
logging.info("Error saving file %s because of %s", img_path, err)
return jsonify({"message": "Error saving file"}), 500
img_exif = yoink(img_path) # Get EXIF data image_colours = ColorThief(save_path).get_palette(color_count=3)
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
# Save to database image_record = Pictures(
query = Pictures(
author_id=current_user.id, author_id=current_user.id,
filename=img_name + "." + img_ext, filename=image_filename,
mimetype=img_ext, mimetype=image_mime,
exif=img_exif, colours=image_colours,
colours=img_colors, description=image_description,
description=form["description"], alt=image_alt,
alt=form["alt"],
) )
db.session.add(image_record)
db.session.commit()
db.session.add(query) image_exif = []
with Image.open(save_path) as file:
image_exif.append(
Exif(
picture_id=image_record.id,
key="FileName",
value=image_filename,
)
)
image_exif.append(
Exif(
picture_id=image_record.id,
key="FileSize",
value=os.path.getsize(save_path),
)
)
image_exif.append(
Exif(
picture_id=image_record.id,
key="FileFormat",
value=image_mime,
)
)
image_exif.append(
Exif(
picture_id=image_record.id,
key="FileWidth",
value=file.size[0],
)
)
image_exif.append(
Exif(
picture_id=image_record.id,
key="FileHeight",
value=file.size[1],
)
)
try:
tags = file._getexif()
for tag, value in TAGS.items():
if tag in tags:
image_exif.append(
Exif(
picture_id=image_record.id,
key=value,
value=tags[tag],
)
)
except TypeError:
pass
db.session.add_all(image_exif)
db.session.commit() db.session.commit()
return jsonify({"message": "File uploaded"}), 200 return jsonify({"message": "File uploaded"}), 200

View file

@ -6,7 +6,7 @@ from flask_login import UserMixin
from onlylegs.extensions import db from onlylegs.extensions import db
class AlbumJunction(db.Model): # pylint: disable=too-few-public-methods, C0103 class AlbumJunction(db.Model):
""" """
Junction table for picturess and albums Junction table for picturess and albums
Joins with picturess and albums Joins with picturess and albums
@ -26,7 +26,7 @@ class AlbumJunction(db.Model): # pylint: disable=too-few-public-methods, C0103
) )
class Pictures(db.Model): # pylint: disable=too-few-public-methods, C0103 class Pictures(db.Model):
""" """
Pictures table Pictures table
""" """
@ -38,7 +38,6 @@ class Pictures(db.Model): # pylint: disable=too-few-public-methods, C0103
filename = db.Column(db.String, unique=True, nullable=False) filename = db.Column(db.String, unique=True, nullable=False)
mimetype = db.Column(db.String, nullable=False) mimetype = db.Column(db.String, nullable=False)
exif = db.Column(db.PickleType, nullable=False)
colours = db.Column(db.PickleType, nullable=False) colours = db.Column(db.PickleType, nullable=False)
description = db.Column(db.String, nullable=False) description = db.Column(db.String, nullable=False)
@ -51,9 +50,24 @@ class Pictures(db.Model): # pylint: disable=too-few-public-methods, C0103
) )
album_fk = db.relationship("AlbumJunction", backref="pictures") album_fk = db.relationship("AlbumJunction", backref="pictures")
exif_fk = db.relationship("Exif", backref="pictures")
class Albums(db.Model): # pylint: disable=too-few-public-methods, C0103 class Exif(db.Model):
"""
Exif data for pictures
"""
__tablename__ = "exif"
id = db.Column(db.Integer, primary_key=True)
picture_id = db.Column(db.Integer, db.ForeignKey("pictures.id"))
key = db.Column(db.String, nullable=False)
value = db.Column(db.String, nullable=False)
class Albums(db.Model):
""" """
albums table albums table
""" """
@ -75,7 +89,7 @@ class Albums(db.Model): # pylint: disable=too-few-public-methods, C0103
album_fk = db.relationship("AlbumJunction", backref="albums") album_fk = db.relationship("AlbumJunction", backref="albums")
class Users(db.Model, UserMixin): # pylint: disable=too-few-public-methods, C0103 class Users(db.Model, UserMixin):
""" """
Users table Users table
""" """

View file

@ -0,0 +1,135 @@
function showContextMenu(obj, menu, position='mouse') {
// If the context menu is already open, close it first
if (document.querySelector(".contextMenu")) {
dissmissContextMenu();
}
// Add span to close the context menu
let contextCloseSpan = document.createElement("span");
contextCloseSpan.className = "contextMenuClose";
contextCloseSpan.onclick = dissmissContextMenu;
// Create the context menu
let contextMenu = document.createElement("div");
contextMenu.className = "contextMenu";
// Create the menu items
menu.forEach(array => {
if (array.value === "divider") {
let divider = document.createElement("hr");
divider.className = "contextMenuDivider";
contextMenu.appendChild(divider);
return;
} else if (array['value'] === "title") {
let titleP = document.createElement("p");
titleP.className = "contextMenuTitle";
titleP.innerHTML = array.text;
contextMenu.appendChild(titleP);
let divider = document.createElement("hr");
divider.className = "contextMenuDivider";
contextMenu.appendChild(divider);
return;
}
let itemBtn = document.createElement("button");
itemBtn.className = "contextMenuItem";
itemBtn.onclick = array['function'];
if (array['type'] === "critical") {
itemBtn.classList.add("contextMenuItem__critical");
} else if (array['type'] === "warning") {
itemBtn.classList.add("contextMenuItem__warning");
} else if (array['type'] === "success") {
itemBtn.classList.add("contextMenuItem__success");
} else if (array['type'] === "info") {
itemBtn.classList.add("contextMenuItem__info");
}
let itemIcon = document.createElement("span");
itemIcon.className = "contextMenuIcon";
if (array['icon']) {
itemIcon.innerHTML = array['icon'];
} else {
itemIcon.innerHTML = '';
}
itemBtn.appendChild(itemIcon);
// Create the text for the action
let itemText = document.createElement("p");
itemText.className = "contextMenuText";
itemText.innerHTML = array.value;
itemBtn.appendChild(itemText);
contextMenu.appendChild(itemBtn);
});
// Add the context menu to the body
document.body.appendChild(contextMenu);
document.body.appendChild(contextCloseSpan);
let posX;
let posY;
if (position === 'mouse') {
posX = event.clientX + 5;
posY = event.clientY + 5;
} else if (position === 'button') {
posX = obj.offsetLeft + (obj.offsetWidth / 2) - (contextMenu.offsetWidth / 2);
posY = obj.offsetTop + obj.offsetHeight + 5;
} else if (position === 'center') {
posX = (window.innerWidth / 2) - (contextMenu.offsetWidth / 2);
posY = (window.innerHeight / 2) - (contextMenu.offsetHeight / 2);
} else {
posX = event.clientX + 5;
posY = event.clientY + 5;
}
// Move the context menu if it is off the screen
if (posX + contextMenu.offsetWidth > window.innerWidth) {
posX = window.innerWidth - (contextMenu.offsetWidth + 5);
} else if (posX < 0) {
posX = 5;
}
if (posY < 0) {
posY = 5;
}
contextMenu.style.left = posX + "px";
contextMenu.style.top = posY + "px";
// Timeout otherwise animation doesn't work
setTimeout(function() {
if (position === 'mouse') {
contextMenu.classList.add("contextMenu__show--mouse");
} else if (position === 'button') {
contextMenu.classList.add("contextMenu__show--button");
} else if (position === 'center') {
contextMenu.classList.add("contextMenu__show--center");
} else {
contextMenu.classList.add("contextMenu__show");
}
}, 1);
}
function dissmissContextMenu() {
// Remove the close span
let contextSpan = document.querySelectorAll(".contextMenuClose");
contextSpan.forEach(menu => {
menu.remove();
});
// Get the context menu
let contextMenu = document.querySelectorAll(".contextMenu");
contextMenu.forEach(menu => {
menu.classList.add("contextMenu__hide");
setTimeout(function() {
menu.remove();
}, 500);
});
}
window.onresize = () => {
dissmissContextMenu();
}

View file

@ -12,6 +12,69 @@ function imageFullscreen() {
document.cookie = "image-info=1" document.cookie = "image-info=1"
} }
} }
function imageShowOptionsPopup(obj) {
// let title = 'Options';
// let subtitle = null;
//
// let body = document.createElement('div');
// body.style.cssText = 'display: flex; flex-direction: column; gap: 0.5rem;';
//
// let copyBtn = document.createElement('button');
// copyBtn.classList.add('btn-block');
// copyBtn.innerHTML = 'Copy URL';
// copyBtn.onclick = () => {
// copyToClipboard(window.location.href)
// }
//
// let downloadBtn = document.createElement('a');
// downloadBtn.classList.add('btn-block');
// downloadBtn.innerHTML = 'Download';
// downloadBtn.href = '/api/media/uploads/' + image_data["filename"];
// downloadBtn.download = '';
//
// body.appendChild(copyBtn);
// body.appendChild(downloadBtn);
//
// if (image_data["owner"]) {
// let editBtn = document.createElement('button');
// editBtn.classList.add('btn-block');
// editBtn.classList.add('critical');
// editBtn.innerHTML = 'Edit';
// editBtn.onclick = imageEditPopup;
//
// let deleteBtn = document.createElement('button');
// deleteBtn.classList.add('btn-block');
// deleteBtn.classList.add('critical');
// deleteBtn.innerHTML = 'Delete';
// deleteBtn.onclick = imageDeletePopup;
//
// body.appendChild(editBtn);
// body.appendChild(deleteBtn);
// }
//
// popupShow(title, subtitle, body, [popupCancelButton]);
showContextMenu(obj, [
{
'value': 'Edit',
'function': () => {
dissmissContextMenu();
imageEditPopup();
},
'type': 'critical'
},
{
'value': 'Delete',
'function': () => {
dissmissContextMenu();
imageDeletePopup();
},
'type': 'critical'
}
], 'button')
}
function imageDeletePopup() { function imageDeletePopup() {
let title = 'DESTRUCTION!!!!!!'; let title = 'DESTRUCTION!!!!!!';
let subtitle = let subtitle =
@ -92,7 +155,6 @@ function imageEditConfirm(event) {
body: form, body: form,
}).then(response => { }).then(response => {
if (response.ok) { if (response.ok) {
popupDismiss();
window.location.reload(); window.location.reload();
} else { } else {
addNotification('Image *clings*', 2); addNotification('Image *clings*', 2);

View file

@ -9,13 +9,17 @@ function popupShow(titleText, subtitleText, bodyContent=null, userActions=null)
actionsSelector.innerHTML = ''; actionsSelector.innerHTML = '';
// Set popup header and subtitle // Set popup header and subtitle
let titleElement = document.createElement('h2'); if (titleText) {
titleElement.innerHTML = titleText; let titleElement = document.createElement('h2');
headerSelector.appendChild(titleElement); titleElement.innerHTML = titleText;
headerSelector.appendChild(titleElement);
}
let subtitleElement = document.createElement('p'); if (subtitleText) {
subtitleElement.innerHTML = subtitleText; let subtitleElement = document.createElement('p');
headerSelector.appendChild(subtitleElement); subtitleElement.innerHTML = subtitleText;
headerSelector.appendChild(subtitleElement);
}
if (bodyContent) { headerSelector.appendChild(bodyContent) } if (bodyContent) { headerSelector.appendChild(bodyContent) }

View file

@ -0,0 +1,128 @@
.contextMenuClose
width: 100%
height: 100%
position: fixed
top: 0
left: 0
background-color: transparent
z-index: 99998
.contextMenu
margin: 0
padding: 0.25rem
width: calc( 100vw - 10px )
height: auto
max-width: 300px
position: absolute
display: flex
flex-direction: column
justify-content: flex-start
align-items: flex-start
gap: 0.25rem
background-color: RGB($bg-300)
border: 1px solid RGB($bg-200)
border-radius: 6px
overflow: hidden
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out
transform-origin: center center
opacity: 0.5
transform: scale(0, 0)
z-index: 99999
.contextMenuTitle
margin: 0
padding: 0.25rem 0.5rem
width: 100%
text-align: center
font-size: 1.2rem
font-weight: 400
color: RGB($fg-white)
.contextMenuItem
margin: 0
padding: 0.5rem
width: 100%
height: auto
display: flex
flex-direction: row
justify-content: flex-start
align-items: center
gap: 0.5rem
background-color: RGB($bg-300)
color: RGB($fg-white)
border: none
border-radius: 3px
cursor: pointer
.contextMenuItem:hover
background-color: RGB($bg-200)
.contextMenuItem__critical
color: RGB($critical)
.contextMenuItem__warning
color: RGB($warning)
.contextMenuItem__success
color: RGB($success)
.contextMenuItem__info
color: RGB($primary)
.contextMenuText
margin: 0
padding: 0
font-size: 1rem
font-weight: 400
.contextMenuIcon
margin: 0
padding: 0
width: 1.25rem
height: 1.25rem
display: flex
justify-content: center
align-items: center
.contextMenuDivider
margin: 0 auto
padding: 0
width: 100%
height: 1px
border: none
background-color: RGB($bg-200)
.contextMenu__show
opacity: 1
transform: scale(1, 1)
.contextMenu__show--mouse
opacity: 1
transform: scale(1, 1)
transform-origin: top left
.contextMenu__show--button
opacity: 1
transform: scale(1, 1)
transform-origin: top center
.contextMenu__show--center
opacity: 1
transform: scale(1, 1)
transform-origin: center center
.contextMenu__hide
opacity: 0
transform: scale(0, 0)

View file

@ -6,6 +6,7 @@
@import "components/notification" @import "components/notification"
@import "components/pop-up" @import "components/pop-up"
@import "components/context-menu"
@import "components/upload-panel" @import "components/upload-panel"
@import "components/tags" @import "components/tags"

View file

@ -13,7 +13,8 @@
'id': {{ image.id }}, 'id': {{ image.id }},
'description': '{{ image.description }}', 'description': '{{ image.description }}',
'alt': '{{ image.alt }}', 'alt': '{{ image.alt }}',
}; 'filename': '{{ image.filename }}',
}
</script> </script>
<style> <style>
@ -31,16 +32,12 @@
<div class="pill-row"> <div class="pill-row">
{% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %} {% if next_url %}<div><a class="pill-item" href="{{ next_url }}"><i class="ph ph-arrow-left"></i></a></div>{% endif %}
<div> <div>
<button class="pill-item" onclick="imageFullscreen()" id="fullscreenImage"><i class="ph ph-info"></i></button> <button class="pill-item" onclick="imageFullscreen()"><i class="ph ph-info"></i></button>
<button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button> <button class="pill-item" onclick="copyToClipboard(window.location.href)"><i class="ph ph-export"></i></button>
<a class="pill-item" href="{{ url_for('api.media', path='uploads/' + image.filename) }}" download onclick="addNotification('Download started!', 4)"><i class="ph ph-file-arrow-down"></i></a> {% if image.author.id == current_user.id %}
<button class="pill-item pill__critical" onclick="imageShowOptionsPopup(this)"><i class="ph-fill ph-dots-three-outline-vertical"></i></button>
{% endif %}
</div> </div>
{% if current_user.id == image.author.id %}
<div>
<button class="pill-item pill__critical" onclick="imageDeletePopup()"><i class="ph ph-trash"></i></button>
<button class="pill-item pill__critical" onclick="imageEditPopup()"><i class="ph ph-pencil-simple"></i></button>
</div>
{% endif %}
{% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %} {% if prev_url %}<div><a class="pill-item" href="{{ prev_url }}"><i class="ph ph-arrow-right"></i></a></div>{% endif %}
</div> </div>
</div> </div>
@ -116,42 +113,20 @@
</div> </div>
{% endif %} {% endif %}
</details> </details>
{% for tag in image.exif %} <details open>
<details open> <summary>
<summary> <i class="ph ph-file-image"></i><h2>Metadata</h2>
{% if tag == 'Photographer' %} <span style="width: 100%"></span>
<i class="ph ph-person"></i><h2>Photographer</h2> <i class="ph ph-caret-down collapse-indicator"></i>
{% elif tag == 'Camera' %} </summary>
<i class="ph ph-camera"></i><h2>Camera</h2> <table>
{% elif tag == 'Software' %} {% for tag in image_exif %}
<i class="ph ph-desktop-tower"></i><h2>Software</h2> <tr>
{% elif tag == 'File' %} <td>{{ tag.key }}</td>
<i class="ph ph-file-image"></i><h2>File</h2> <td>{{ tag.value }}</td>
{% else %} </tr>
<i class="ph ph-file-image"></i><h2>{{ tag }}</h2> {% endfor %}
{% endif %} </table>
<span style="width: 100%"></span> </details>
<i class="ph ph-caret-down collapse-indicator"></i>
</summary>
<table>
{% for subtag in image.exif[tag] %}
<tr>
<td>{{ subtag }}</td>
{% if image.exif[tag][subtag]['formatted'] %}
{% if image.exif[tag][subtag]['type'] == 'date' %}
<td><span class="time">{{ image.exif[tag][subtag]['formatted'] }}</span></td>
{% else %}
<td>{{ image.exif[tag][subtag]['formatted'] }}</td>
{% endif %}
{% elif image.exif[tag][subtag]['raw'] %}
<td>{{ image.exif[tag][subtag]['raw'] }}</td>
{% else %}
<td class="empty-table">Oops, an error</td>
{% endif %}
</tr>
{% endfor %}
</table>
</details>
{% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -15,7 +15,7 @@ from flask import (
jsonify, jsonify,
) )
from flask_login import current_user from flask_login import current_user
from onlylegs.models import Pictures, AlbumJunction, Albums from onlylegs.models import Pictures, AlbumJunction, Albums, Exif
from onlylegs.extensions import db from onlylegs.extensions import db
@ -28,7 +28,8 @@ def image(image_id):
Image view, shows the image and its metadata Image view, shows the image and its metadata
""" """
# Get the image, if it doesn't exist, 404 # Get the image, if it doesn't exist, 404
image = db.get_or_404(Pictures, image_id, description="Image not found :<") image_record = db.get_or_404(Pictures, image_id, description="Image not found :<")
image_record_exif = Exif.query.filter(Exif.picture_id == image_id).all()
# Get all groups the image is in # Get all groups the image is in
groups = ( groups = (
@ -38,9 +39,9 @@ def image(image_id):
) )
# Get the group data for each group the image is in # Get the group data for each group the image is in
image.groups = [] image_record.groups = []
for group in groups: for group in groups:
image.groups.append( image_record.groups.append(
Albums.query.with_entities(Albums.id, Albums.name) Albums.query.with_entities(Albums.id, Albums.name)
.filter(Albums.id == group[0]) .filter(Albums.id == group[0])
.first() .first()
@ -72,9 +73,8 @@ def image(image_id):
limit = current_app.config["UPLOAD_CONF"]["max-load"] limit = current_app.config["UPLOAD_CONF"]["max-load"]
# If the number of items is less than the limit, no point of calculating the page # If the number of items is less than the limit, no point of calculating the page
if len(total_images) <= limit: return_page = None
return_page = None if len(total_images) > limit:
else:
# How many pages should there be # How many pages should there be
for i in range(ceil(len(total_images) / limit)): for i in range(ceil(len(total_images) / limit)):
# Slice the list of IDs into chunks of the limit # Slice the list of IDs into chunks of the limit
@ -90,7 +90,8 @@ def image(image_id):
return render_template( return render_template(
"image.html", "image.html",
image=image, image=image_record,
image_exif=image_record_exif,
next_url=next_url, next_url=next_url,
prev_url=prev_url, prev_url=prev_url,
return_page=return_page, return_page=return_page,