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
from uuid import uuid4
from PIL import Image
from PIL.ExifTags import TAGS
from flask import (
Blueprint,
abort,
@ -20,8 +23,7 @@ from flask_login import login_required, current_user
from colorthief import ColorThief
from onlylegs.extensions import db
from onlylegs.models import Users, Pictures
from onlylegs.utils.metadata import yoink
from onlylegs.models import Users, Pictures, Exif
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
"""
form_file = request.files.get("file", None)
form = request.form
image_description = request.form.get("description", "").strip()
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
# Get file extension, generate random name and set file path
img_ext = pathlib.Path(form_file.filename).suffix.replace(".", "").lower()
img_name = "GWAGWA_" + str(uuid4())
img_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], img_name + "." + img_ext
)
image_mime = pathlib.Path(image_file.filename).suffix.replace(".", "").lower()
image_name = "GWAGWA_" + str(uuid4())
image_filename = image_name + "." + image_mime
save_path = os.path.join(current_app.config["UPLOAD_FOLDER"], image_filename)
# Check if file extension is allowed
if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info("File extension not allowed: %s", img_ext)
if image_mime not in current_app.config["ALLOWED_EXTENSIONS"].keys():
logging.info("File extension not allowed: %s", image_mime)
return jsonify({"message": "File extension not allowed"}), 403
# Save file
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
image_file.save(save_path)
img_exif = yoink(img_path) # Get EXIF data
img_colors = ColorThief(img_path).get_palette(color_count=3) # Get color palette
image_colours = ColorThief(save_path).get_palette(color_count=3)
# Save to database
query = Pictures(
image_record = Pictures(
author_id=current_user.id,
filename=img_name + "." + img_ext,
mimetype=img_ext,
exif=img_exif,
colours=img_colors,
description=form["description"],
alt=form["alt"],
filename=image_filename,
mimetype=image_mime,
colours=image_colours,
description=image_description,
alt=image_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()
return jsonify({"message": "File uploaded"}), 200

View file

@ -6,7 +6,7 @@ from flask_login import UserMixin
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
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
"""
@ -38,7 +38,6 @@ class Pictures(db.Model): # pylint: disable=too-few-public-methods, C0103
filename = db.Column(db.String, unique=True, nullable=False)
mimetype = db.Column(db.String, nullable=False)
exif = db.Column(db.PickleType, nullable=False)
colours = db.Column(db.PickleType, 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")
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
"""
@ -75,7 +89,7 @@ class Albums(db.Model): # pylint: disable=too-few-public-methods, C0103
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
"""

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"
}
}
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() {
let title = 'DESTRUCTION!!!!!!';
let subtitle =
@ -92,7 +155,6 @@ function imageEditConfirm(event) {
body: form,
}).then(response => {
if (response.ok) {
popupDismiss();
window.location.reload();
} else {
addNotification('Image *clings*', 2);

View file

@ -9,13 +9,17 @@ function popupShow(titleText, subtitleText, bodyContent=null, userActions=null)
actionsSelector.innerHTML = '';
// Set popup header and subtitle
let titleElement = document.createElement('h2');
titleElement.innerHTML = titleText;
headerSelector.appendChild(titleElement);
if (titleText) {
let titleElement = document.createElement('h2');
titleElement.innerHTML = titleText;
headerSelector.appendChild(titleElement);
}
let subtitleElement = document.createElement('p');
subtitleElement.innerHTML = subtitleText;
headerSelector.appendChild(subtitleElement);
if (subtitleText) {
let subtitleElement = document.createElement('p');
subtitleElement.innerHTML = subtitleText;
headerSelector.appendChild(subtitleElement);
}
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/pop-up"
@import "components/context-menu"
@import "components/upload-panel"
@import "components/tags"

View file

@ -13,7 +13,8 @@
'id': {{ image.id }},
'description': '{{ image.description }}',
'alt': '{{ image.alt }}',
};
'filename': '{{ image.filename }}',
}
</script>
<style>
@ -31,16 +32,12 @@
<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 %}
<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>
<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>
{% 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 %}
</div>
</div>
@ -116,42 +113,20 @@
</div>
{% endif %}
</details>
{% for tag in image.exif %}
<details open>
<summary>
{% if tag == 'Photographer' %}
<i class="ph ph-person"></i><h2>Photographer</h2>
{% elif tag == 'Camera' %}
<i class="ph ph-camera"></i><h2>Camera</h2>
{% elif tag == 'Software' %}
<i class="ph ph-desktop-tower"></i><h2>Software</h2>
{% elif tag == 'File' %}
<i class="ph ph-file-image"></i><h2>File</h2>
{% else %}
<i class="ph ph-file-image"></i><h2>{{ tag }}</h2>
{% endif %}
<span style="width: 100%"></span>
<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 %}
<details open>
<summary>
<i class="ph ph-file-image"></i><h2>Metadata</h2>
<span style="width: 100%"></span>
<i class="ph ph-caret-down collapse-indicator"></i>
</summary>
<table>
{% for tag in image_exif %}
<tr>
<td>{{ tag.key }}</td>
<td>{{ tag.value }}</td>
</tr>
{% endfor %}
</table>
</details>
</div>
{% endblock %}

View file

@ -15,7 +15,7 @@ from flask import (
jsonify,
)
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
@ -28,7 +28,8 @@ def image(image_id):
Image view, shows the image and its metadata
"""
# 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
groups = (
@ -38,9 +39,9 @@ def image(image_id):
)
# Get the group data for each group the image is in
image.groups = []
image_record.groups = []
for group in groups:
image.groups.append(
image_record.groups.append(
Albums.query.with_entities(Albums.id, Albums.name)
.filter(Albums.id == group[0])
.first()
@ -72,9 +73,8 @@ def image(image_id):
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 len(total_images) <= limit:
return_page = None
else:
return_page = None
if len(total_images) > limit:
# How many pages should there be
for i in range(ceil(len(total_images) / limit)):
# Slice the list of IDs into chunks of the limit
@ -90,7 +90,8 @@ def image(image_id):
return render_template(
"image.html",
image=image,
image=image_record,
image_exif=image_record_exif,
next_url=next_url,
prev_url=prev_url,
return_page=return_page,