Ethan Dalool
4c65ccaf68
I found that the strict heirarchy was not satisfying the situation where one tag is the intersection of two others, but we can only pick one as the parent For example, does red_jacket belong under clothes.red_clothes or clothes.jackets? A search for "red_clothes AND jackets" might give us someone wearing red pants and a black jacket, so this definitely needs to be a separate tag, but picking only one parent for it is not sufficient. Now, a search for red_clothes and a search for jackets will both find our red_jacket photo. The change also applies to Albums because why not, and I'm sure a similar case can be made. Unfortunately this means tags no longer have one true qualname. The concept of qualnames has not been completely phased out but it's in progress. This commit is very big because I was not sure for a long time whether to go through with it, and so much stuff had to change that I don't want to go back and figure out what could be grouped together.
455 lines
12 KiB
HTML
455 lines
12 KiB
HTML
<!DOCTYPE html5>
|
|
<html>
|
|
<head>
|
|
{% import "header.html" as header %}
|
|
{% import "tag_object.html" as tag_object %}
|
|
<title>{{photo.basename}} | Photos</title>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<link rel="stylesheet" href="/static/css/common.css">
|
|
<script src="/static/js/common.js"></script>
|
|
<script src="/static/js/hotkeys.js"></script>
|
|
<script src="/static/js/photoclipboard.js"></script>
|
|
|
|
<style>
|
|
#content_body
|
|
{
|
|
/* Override common.css */
|
|
flex: 1;
|
|
flex-direction: row;
|
|
}
|
|
#left
|
|
{
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
#right
|
|
{
|
|
display: flex;
|
|
flex: 1;
|
|
}
|
|
#editor_holder
|
|
{
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
max-width: 300px;
|
|
/*padding: 8px;*/
|
|
|
|
}
|
|
#editor_area
|
|
{
|
|
flex: 3;
|
|
padding: 8px;
|
|
|
|
background-color: rgba(0, 0, 0, 0.1);
|
|
|
|
word-wrap: break-word;
|
|
}
|
|
#message_area_bg
|
|
{
|
|
display: flex;
|
|
flex: 0 1 100%;
|
|
|
|
height: 100%;
|
|
min-height: 30px;
|
|
padding: 8px;
|
|
|
|
background-color: rgba(0, 0, 0, 0.1);
|
|
}
|
|
#message_area
|
|
{
|
|
flex: auto;
|
|
|
|
max-height: none;
|
|
}
|
|
.photo_viewer
|
|
{
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.photo_viewer a
|
|
{
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
#photo_img_holder
|
|
{
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
height: 100%;
|
|
width: 100%;
|
|
|
|
background-repeat: no-repeat;
|
|
}
|
|
#photo_img_holder img
|
|
{
|
|
max-height: 100%;
|
|
max-width: 100%;
|
|
}
|
|
.photo_viewer audio
|
|
{
|
|
width: 100%;
|
|
}
|
|
.photo_viewer video
|
|
{
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
}
|
|
#refresh_metadata_button
|
|
{
|
|
font-size: 11px;
|
|
}
|
|
@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;
|
|
width: initial;
|
|
max-width: none;
|
|
margin-top: 8px;
|
|
}
|
|
#right
|
|
{
|
|
flex: none;
|
|
height: calc(100% - 20px);
|
|
}
|
|
#editor_holder
|
|
{
|
|
display: flex;
|
|
flex-direction: row;
|
|
max-width: none;
|
|
}
|
|
#message_area
|
|
{
|
|
flex: 2;
|
|
height: initial;
|
|
max-height: none;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
|
|
<body>
|
|
{{header.make_header(session=session)}}
|
|
<div id="content_body">
|
|
<div id="left">
|
|
<div id="editor_holder">
|
|
<div id="editor_area">
|
|
<!-- TAG INFO -->
|
|
<h4>Tags</h4>
|
|
<ul id="this_tags">
|
|
<li>
|
|
<input id="add_tag_textbox" type="text" autofocus>
|
|
<button id="add_tag_button" class="green_button" onclick="submit_tag(receive_callback);">add</button>
|
|
</li>
|
|
{% set tags = photo.get_tags()|sort_tags %}
|
|
{% for tag in tags %}
|
|
<li>
|
|
{{tag_object.tag_object(tag, with_alt_description=True)}}<!--
|
|
--><button
|
|
class="remove_tag_button red_button"
|
|
onclick="remove_photo_tag('{{photo.id}}', '{{tag.name}}', receive_callback);">
|
|
</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<!-- METADATA & DOWNLOAD -->
|
|
<h4>
|
|
File info
|
|
<button id="refresh_metadata_button" class="green_button" onclick="refresh_metadata('{{photo.id}}');">refresh</button>
|
|
</h4>
|
|
<ul id="metadata">
|
|
<li>Filename: {{photo.basename}}</li>
|
|
{% set author = photo.get_author() %}
|
|
{% if author is not none %}
|
|
<li>Author: <a href="/user/{{author.username}}">{{author.display_name}}</a></li>
|
|
{% endif %}
|
|
{% if photo.width %}
|
|
<li title="{{photo.area}} px">Dimensions: {{photo.width}}x{{photo.height}} px</li>
|
|
<li>Aspect ratio: {{photo.ratio}}</li>
|
|
{% endif %}
|
|
<li>Size: {{photo.bytes|bytestring}}</li>
|
|
{% if photo.duration %}
|
|
<li>Duration: {{photo.duration_string}}</li>
|
|
<li>Overall Bitrate: {{photo.bitrate|int}} kbps</li>
|
|
{% endif %}
|
|
<li><a href="{{photo|file_link}}?download=true&original_filename=true">Download as original filename</a></li>
|
|
<li><a href="{{photo|file_link}}?download=true">Download as {{photo.id}}.{{photo.extension}}</a></li>
|
|
</ul>
|
|
|
|
<div>
|
|
<label class="photo_card" data-id="{{photo.id}}"><input type="checkbox" class="photo_card_selector_checkbox" onclick="on_photo_select(event)"/>Clipboard</label>
|
|
</div>
|
|
|
|
<!-- CONTAINING ALBUMS -->
|
|
{% set albums = photo.get_containing_albums() %}
|
|
{% if albums %}
|
|
<h4>Albums containing this photo</h4>
|
|
<ul id="containing albums">
|
|
{% for album in albums %}
|
|
<li><a href="/album/{{album.id}}">{{album.display_name}}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
|
|
<div>
|
|
<a href="/search?created=-{{photo.created}}">←Before</a>
|
|
<span> | </span>
|
|
<a href="/search?created={{photo.created}}-&orderby=created-asc">After→</a>
|
|
</div>
|
|
</div>
|
|
<div id="message_area_bg">
|
|
<div id="message_area">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="right">
|
|
<!-- THE PHOTO ITSELF -->
|
|
<div class="photo_viewer">
|
|
{% if photo.simple_mimetype == "image" %}
|
|
<div id="photo_img_holder" onclick="toggle_hoverzoom(event)">
|
|
<img
|
|
id="photo_img"
|
|
src="{{photo|file_link}}"
|
|
alt="{{photo.basename}}"
|
|
onload="this.style.opacity=0.99"
|
|
>
|
|
</div>
|
|
{% elif photo.simple_mimetype == "video" %}
|
|
<video src="{{photo|file_link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video>
|
|
{% elif photo.simple_mimetype == "audio" %}
|
|
<audio src="{{photo|file_link}}" controls></audio>
|
|
{% else %}
|
|
<a href="{{photo|file_link}}">View {{photo.basename}}</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
|
|
|
|
<script type="text/javascript">
|
|
var content_body = document.getElementById('content_body');
|
|
var add_tag_box = document.getElementById('add_tag_textbox');
|
|
var add_tag_button = document.getElementById('add_tag_button');
|
|
var message_area = document.getElementById('message_area');
|
|
add_tag_box.addEventListener("keyup", entry_with_history_hook);
|
|
bind_box_to_button(add_tag_box, add_tag_button, false);
|
|
|
|
photo_img_holder = document.getElementById("photo_img_holder");
|
|
photo_img = document.getElementById("photo_img");
|
|
|
|
function add_photo_tag(photoid, tagname, callback)
|
|
{
|
|
if (tagname === ""){return}
|
|
var url = "/photo/" + photoid + "/add_tag";
|
|
var data = new FormData();
|
|
data.append("tagname", tagname);
|
|
return post(url, data, callback);
|
|
}
|
|
function remove_photo_tag(photoid, tagname, callback)
|
|
{
|
|
if (tagname === ""){return}
|
|
var url = "/photo/" + photoid + "/remove_tag";
|
|
var data = new FormData();
|
|
data.append("tagname", tagname);
|
|
return post(url, data, callback);
|
|
}
|
|
function submit_tag(callback)
|
|
{
|
|
add_photo_tag('{{photo.id}}', add_tag_box.value, callback);
|
|
add_tag_box.value = "";
|
|
}
|
|
function receive_callback(response)
|
|
{
|
|
var message_text;
|
|
var message_positivity;
|
|
var tagname = response["data"]["tagname"];
|
|
if ("error_type" in response["data"])
|
|
{
|
|
message_positivity = "message_negative";
|
|
message_text = response["data"]["error_message"];
|
|
}
|
|
else
|
|
{
|
|
var action;
|
|
message_positivity = "message_positive";
|
|
if (response["meta"]["request_url"].includes("add_tag"))
|
|
{
|
|
message_text = "Added tag " + tagname;
|
|
}
|
|
else if (response["meta"]["request_url"].includes("remove_tag"))
|
|
{
|
|
message_text = "Removed tag " + tagname;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
create_message_bubble(message_area, message_positivity, message_text, 8000);
|
|
}
|
|
|
|
function refresh_metadata(photoid)
|
|
{
|
|
var url= "/photo/" + photoid + "/refresh_metadata";
|
|
var data = new FormData();
|
|
var callback = function(){location.reload();};
|
|
post(url, data, callback);
|
|
}
|
|
|
|
var ZOOM_BG_URL = "url('{{photo|file_link}}')";
|
|
function enable_hoverzoom(event)
|
|
{
|
|
//console.log("enable zoom");
|
|
photo_img_holder = document.getElementById("photo_img_holder");
|
|
photo_img = document.getElementById("photo_img");
|
|
if (
|
|
photo_img.naturalWidth < photo_img_holder.offsetWidth &&
|
|
photo_img.naturalHeight < photo_img_holder.offsetHeight
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
photo_img.style.opacity = "0";
|
|
photo_img.style.display = "none";
|
|
photo_img_holder.style.cursor = "zoom-out";
|
|
photo_img_holder.style.backgroundImage = ZOOM_BG_URL;
|
|
photo_img_holder.onmousemove = move_hoverzoom;
|
|
move_hoverzoom(event)
|
|
//setTimeout(function(){img_holder.onclick = toggle_hoverzoom;}, 100);
|
|
return true;
|
|
}
|
|
function disable_hoverzoom()
|
|
{
|
|
//console.log("disable zoom");
|
|
photo_img.style.opacity = "100";
|
|
photo_img_holder.style.cursor = "";
|
|
photo_img.style.display = "";
|
|
photo_img_holder.style.backgroundImage = "none";
|
|
photo_img_holder.onmousemove = null;
|
|
//photo_img_holder.onclick = null;
|
|
}
|
|
function toggle_hoverzoom()
|
|
{
|
|
if (photo_img.style.opacity === "0")
|
|
{
|
|
disable_hoverzoom();
|
|
}
|
|
else
|
|
{
|
|
enable_hoverzoom(event);
|
|
}
|
|
if (getComputedStyle(content_body).flexDirection != "column-reverse")
|
|
{
|
|
add_tag_box.focus();
|
|
}
|
|
}
|
|
|
|
function move_hoverzoom(event)
|
|
{
|
|
var x;
|
|
var y;
|
|
|
|
/*
|
|
When clicking on the image, the event handler takes the image as the event
|
|
target even though the handler was assigned to the holder. The coordinates
|
|
for the zoom need to be based on the holder, so when this happens we need
|
|
to adjust the numbers.
|
|
I'm not sure why the offset is the holder's offsetLeft. It seems that when
|
|
the event triggers on the holder, the event X is based on its bounding box,
|
|
but when it triggers on the image it's based on the viewport.
|
|
*/
|
|
var mouse_x = event.offsetX;
|
|
var mouse_y = event.offsetY;
|
|
if (event.target !== photo_img_holder)
|
|
{
|
|
mouse_x -= photo_img_holder.offsetLeft;
|
|
mouse_y -= photo_img_holder.offsetTop;
|
|
}
|
|
//console.log(mouse_x);
|
|
|
|
/*
|
|
Adding 5% to perceived position gives us a bit of padding around the image,
|
|
so you don't need to navigate a 1px line to see the edge.
|
|
We first subtract half of the image dimensions so that the 5% is applied
|
|
to both left and right. Otherwise 105% of 0 is still 0 which doesn't
|
|
apply padding on the left.
|
|
*/
|
|
mouse_x -= (photo_img_holder.offsetWidth / 2);
|
|
mouse_x *= 1.05;
|
|
mouse_x += (photo_img_holder.offsetWidth / 2);
|
|
|
|
mouse_y -= (photo_img_holder.offsetHeight / 2);
|
|
mouse_y *= 1.05;
|
|
mouse_y += (photo_img_holder.offsetHeight / 2);
|
|
|
|
if (photo_img.naturalWidth < photo_img_holder.offsetWidth)
|
|
{
|
|
// If the image is smaller than the frame, just center it
|
|
x = (photo_img.naturalWidth - photo_img_holder.offsetWidth) / 2;
|
|
}
|
|
else
|
|
{
|
|
// Take the amount of movement necessary (frame width - image width)
|
|
// times our distance across the image as a percentage.
|
|
x = (photo_img.naturalWidth - photo_img_holder.offsetWidth) * (mouse_x / photo_img_holder.offsetWidth);
|
|
}
|
|
|
|
if (photo_img.naturalHeight < photo_img_holder.offsetHeight)
|
|
{
|
|
y = (photo_img.naturalHeight - photo_img_holder.offsetHeight) / 2;
|
|
}
|
|
else
|
|
{
|
|
y = (photo_img.naturalHeight - photo_img_holder.offsetHeight) * (mouse_y / photo_img_holder.offsetHeight);
|
|
}
|
|
//console.log(x);
|
|
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>
|
|
</html>
|