543 lines
16 KiB
HTML
543 lines
16 KiB
HTML
|
<!DOCTYPE html5>
|
||
|
<html>
|
||
|
<head>
|
||
|
{% import "header.html" as header %}
|
||
|
{% import "clipboard_tray.html" as clipboard_tray %}
|
||
|
<title>Swipe</title>
|
||
|
<meta charset="UTF-8">
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
|
<link rel="stylesheet" href="/static/css/common.css">
|
||
|
<link rel="stylesheet" href="/static/css/etiquette.css">
|
||
|
<link rel="stylesheet" href="/static/css/cards.css">
|
||
|
<link rel="stylesheet" href="/static/css/clipboard_tray.css">
|
||
|
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
|
||
|
<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/photo_clipboard.js"></script>
|
||
|
<script src="/static/js/spinner.js"></script>
|
||
|
<script src="/static/js/tag_autocomplete.js"></script>
|
||
|
|
||
|
<style>
|
||
|
#content_body
|
||
|
{
|
||
|
display: grid;
|
||
|
grid-template:
|
||
|
"photo_viewer right" 1fr
|
||
|
"upcoming_area right" auto
|
||
|
"button_bar button_bar" auto
|
||
|
/1fr 300px;
|
||
|
/* header=18+8+8 + content_body margin-bottom=8 */
|
||
|
height: calc(100vh - 42px);
|
||
|
}
|
||
|
#right
|
||
|
{
|
||
|
display: grid;
|
||
|
grid-template:
|
||
|
"name_tag" auto
|
||
|
"message_area" 1fr
|
||
|
/ auto;
|
||
|
grid-row-gap: 8px;
|
||
|
}
|
||
|
#name_tag
|
||
|
{
|
||
|
grid-area: name_tag;
|
||
|
word-break: break-word;
|
||
|
}
|
||
|
#upcoming_area
|
||
|
{
|
||
|
grid-area: upcoming_area;
|
||
|
display: flex;
|
||
|
flex-direction: row;
|
||
|
align-items: center;
|
||
|
overflow: hidden;
|
||
|
min-height: 0;
|
||
|
}
|
||
|
#message_area
|
||
|
{
|
||
|
grid-area: message_area;
|
||
|
}
|
||
|
#photo_viewer
|
||
|
{
|
||
|
grid-area: photo_viewer;
|
||
|
display: flex;
|
||
|
justify-content: center;
|
||
|
align-items: center;
|
||
|
min-height: 0;
|
||
|
}
|
||
|
#photo_viewer img
|
||
|
{
|
||
|
min-height: 0;
|
||
|
max-height: 100%;
|
||
|
}
|
||
|
#button_bar
|
||
|
{
|
||
|
grid-area: button_bar;
|
||
|
display: flex;
|
||
|
height: 100px;
|
||
|
}
|
||
|
#button_bar > .action_button
|
||
|
{
|
||
|
flex: 1;
|
||
|
}
|
||
|
#button_bar select,
|
||
|
#button_bar input
|
||
|
{
|
||
|
background-color: white;
|
||
|
}
|
||
|
</style>
|
||
|
</head>
|
||
|
|
||
|
<body>
|
||
|
{{header.make_header(session=session)}}
|
||
|
<div id="content_body">
|
||
|
<div id="right" class="panel">
|
||
|
<a id="name_tag" target="_blank">Swipe!</a>
|
||
|
<div id="message_area">
|
||
|
</div>
|
||
|
</div>
|
||
|
<div id="photo_viewer">
|
||
|
<img id="photo_viewer_img" onload="return onload_img(event);" src=""/>
|
||
|
</div>
|
||
|
<div id="upcoming_area">
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
<img class="upcoming_img" src="" width="75px" height="75px"/>
|
||
|
</div>
|
||
|
<div id="button_bar" class="panel">
|
||
|
<button class="action_button red_button" data-action-index="left">
|
||
|
←
|
||
|
<select class="action_select">
|
||
|
<option value="do_nothing">Do nothing</option>
|
||
|
<option value="add_clipboard">Add to clipboard</option>
|
||
|
<option value="add_tag">Add tag</option>
|
||
|
<option value="remove_tag">Remove tag</option>
|
||
|
</select>
|
||
|
<input type="text" class="action_tag_input hidden entry_with_tagname_replacements" list="tag_autocomplete_datalist"/>
|
||
|
</button>
|
||
|
<button class="action_button yellow_button" data-action-index="down">Do nothing</button>
|
||
|
<button class="action_button green_button" data-action-index="right">
|
||
|
<select class="action_select">
|
||
|
<option value="do_nothing">Do nothing</option>
|
||
|
<option value="add_clipboard">Add to clipboard</option>
|
||
|
<option value="add_tag">Add tag</option>
|
||
|
<option value="remove_tag">Remove tag</option>
|
||
|
</select>
|
||
|
<input type="text" class="action_tag_input hidden entry_with_tagname_replacements" list="tag_autocomplete_datalist"/>
|
||
|
→
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
{{clipboard_tray.clipboard_tray()}}
|
||
|
</body>
|
||
|
|
||
|
<script type="text/javascript">
|
||
|
const original_search_params = Array.from(new URLSearchParams(window.location.search));
|
||
|
|
||
|
const name_tag = document.getElementById("name_tag");
|
||
|
const message_area = document.getElementById("message_area");
|
||
|
const photo_viewer_img = document.getElementById("photo_viewer_img");
|
||
|
const upcoming_imgs = document.getElementsByClassName("upcoming_img");
|
||
|
|
||
|
const action_map = {};
|
||
|
for (button of document.getElementsByClassName("action_button"))
|
||
|
{
|
||
|
action_map[button.dataset.actionIndex] = {
|
||
|
"action": "do_nothing",
|
||
|
"tag": null,
|
||
|
"button": button,
|
||
|
"tag_input": button.getElementsByClassName("action_tag_input")[0],
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const photo_queue = [];
|
||
|
const rewind_queue = [];
|
||
|
const REWIND_QUEUE_LENGTH = 15;
|
||
|
|
||
|
let current_photo = null;
|
||
|
let SEARCH_LIMIT = 100;
|
||
|
let current_search_offset = 0;
|
||
|
let waiting_for_img = false;
|
||
|
let pending_search_request = null;
|
||
|
|
||
|
// STATE ///////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
function get_more_photos()
|
||
|
{
|
||
|
// Prevents multiple calls from requesting duplicate ranges.
|
||
|
if (pending_search_request !== null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
function callback(response)
|
||
|
{
|
||
|
pending_search_request = null;
|
||
|
if (! response.meta.json_ok)
|
||
|
{
|
||
|
alert(JSON.stringify(response));
|
||
|
return;
|
||
|
}
|
||
|
if (response.data.error_type)
|
||
|
{
|
||
|
alert(response.data.error_type + "\n" + response.data.error_message);
|
||
|
return;
|
||
|
}
|
||
|
const need_show_photo = photo_queue.length === 0;
|
||
|
const results = response.data.results;
|
||
|
console.log("Got " + results.length + " more photos.");
|
||
|
photo_queue.push(...results);
|
||
|
|
||
|
if (results.length === 0)
|
||
|
{
|
||
|
if (current_search_offset === 0)
|
||
|
{
|
||
|
console.log("Search results seem to be exhausted.");
|
||
|
return;
|
||
|
}
|
||
|
current_search_offset = 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
current_search_offset += results.length;
|
||
|
}
|
||
|
waiting_for_more_photos = false;
|
||
|
|
||
|
if (need_show_photo)
|
||
|
{
|
||
|
show_next_photo();
|
||
|
}
|
||
|
}
|
||
|
console.log("Requesting more photos.");
|
||
|
const search_params = modify_search_params();
|
||
|
pending_search_request = api.photos.search(search_params, callback);
|
||
|
}
|
||
|
|
||
|
function modify_search_params()
|
||
|
{
|
||
|
const search_params = new URLSearchParams();
|
||
|
|
||
|
let extra_musts = [];
|
||
|
let extra_forbids = [];
|
||
|
|
||
|
for (action_index in action_map)
|
||
|
{
|
||
|
const action = action_map[action_index]["action"];
|
||
|
const tag = action_map[action_index]["tag"];
|
||
|
if (action === "remove_tag")
|
||
|
{
|
||
|
extra_must.push(tag);
|
||
|
}
|
||
|
if (action === "add_tag")
|
||
|
{
|
||
|
extra_forbids.push(tag);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extra_musts = extra_musts.join(",");
|
||
|
extra_forbids = extra_forbids.join(",");
|
||
|
|
||
|
let had_musts = false;
|
||
|
let had_forbids = false;
|
||
|
for ([key, value] of original_search_params)
|
||
|
{
|
||
|
if (key === "limit" || key === "offset" || key === "yield_albums" || key === "yield_photos")
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
if (key === "tag_musts")
|
||
|
{
|
||
|
value = value + "," + extra_musts;
|
||
|
}
|
||
|
if (key === "tag_forbids")
|
||
|
{
|
||
|
value = value + "," + extra_forbids;
|
||
|
}
|
||
|
search_params.set(key, value);
|
||
|
}
|
||
|
if (! had_musts && extra_musts.length > 0)
|
||
|
{
|
||
|
search_params.set("tag_musts", extra_musts);
|
||
|
}
|
||
|
if (! had_forbids && extra_forbids.length > 0)
|
||
|
{
|
||
|
search_params.set("tag_forbids", extra_forbids);
|
||
|
}
|
||
|
search_params.set("yield_albums", "no");
|
||
|
search_params.set("yield_photos", "yes");
|
||
|
search_params.set("limit", SEARCH_LIMIT);
|
||
|
search_params.set("offset", current_search_offset);
|
||
|
console.log("Updated search params " + search_params.toString());
|
||
|
return search_params;
|
||
|
}
|
||
|
|
||
|
function onload_img(event)
|
||
|
{
|
||
|
waiting_for_img = false;
|
||
|
}
|
||
|
|
||
|
function reset_swipe()
|
||
|
{
|
||
|
current_photo = null;
|
||
|
photo_queue.length = 0;
|
||
|
rewind_queue.length = 0;
|
||
|
if (pending_search_request !== null)
|
||
|
{
|
||
|
pending_search_request.abort();
|
||
|
pending_search_request = null;
|
||
|
}
|
||
|
current_search_offset = 0;
|
||
|
get_more_photos();
|
||
|
}
|
||
|
|
||
|
// ACTION PROCESSING ///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
function add_remove_tag_callback(response)
|
||
|
{
|
||
|
if (! response.meta.json_ok)
|
||
|
{
|
||
|
alert(JSON.stringify(response));
|
||
|
return;
|
||
|
}
|
||
|
let message_text;
|
||
|
let message_positivity;
|
||
|
let abort;
|
||
|
if ("error_type" in response.data)
|
||
|
{
|
||
|
message_positivity = "message_negative";
|
||
|
message_text = response.data.error_message;
|
||
|
abort = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
const tagname = response.data.tagname;
|
||
|
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;
|
||
|
}
|
||
|
abort = false;
|
||
|
}
|
||
|
common.create_message_bubble(message_area, message_positivity, message_text, 8000);
|
||
|
return abort;
|
||
|
}
|
||
|
|
||
|
function process_current_photo(action, action_tag)
|
||
|
{
|
||
|
if (current_photo === null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
console.log("Doing " + action + " to " + current_photo.id);
|
||
|
if (action === "do_nothing")
|
||
|
{
|
||
|
;
|
||
|
}
|
||
|
if (action === "add_clipboard")
|
||
|
{
|
||
|
photo_clipboard.clipboard.add(current_photo.id);
|
||
|
setTimeout(() => photo_clipboard.save_clipboard(), 0);
|
||
|
}
|
||
|
if (action === "add_tag")
|
||
|
{
|
||
|
if (action_tag === null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
api.photos.add_tag(current_photo.id, action_tag, add_remove_tag_callback);
|
||
|
}
|
||
|
if (action === "remove_tag")
|
||
|
{
|
||
|
if (action_tag === null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
api.photos.remove_tag(current_photo.id, action_tag, add_remove_tag_callback);
|
||
|
}
|
||
|
show_next_photo();
|
||
|
}
|
||
|
|
||
|
// UI //////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
function step_previous_photo()
|
||
|
{
|
||
|
const rewind_photo = rewind_queue.shift();
|
||
|
if (rewind_photo === undefined)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
if (current_photo !== null)
|
||
|
{
|
||
|
photo_queue.unshift(current_photo);
|
||
|
}
|
||
|
current_photo = rewind_photo;
|
||
|
}
|
||
|
|
||
|
function step_next_photo()
|
||
|
{
|
||
|
if (current_photo !== null)
|
||
|
{
|
||
|
rewind_queue.unshift(current_photo);
|
||
|
rewind_queue.length = REWIND_QUEUE_LENGTH;
|
||
|
}
|
||
|
if (photo_queue.length == 0)
|
||
|
{
|
||
|
current_photo = null;
|
||
|
get_more_photos();
|
||
|
return;
|
||
|
}
|
||
|
current_photo = photo_queue.shift();
|
||
|
if (photo_queue.length < 20)
|
||
|
{
|
||
|
get_more_photos();
|
||
|
}
|
||
|
}
|
||
|
function show_previous_photo()
|
||
|
{
|
||
|
step_previous_photo();
|
||
|
show_current_photo();
|
||
|
}
|
||
|
|
||
|
function show_next_photo()
|
||
|
{
|
||
|
step_next_photo();
|
||
|
show_current_photo();
|
||
|
}
|
||
|
|
||
|
function show_current_photo()
|
||
|
{
|
||
|
if (current_photo === null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
name_tag.innerText = current_photo.filename;
|
||
|
name_tag.href = "/photo/" + current_photo.id;
|
||
|
if (current_photo.has_thumbnail)
|
||
|
{
|
||
|
photo_viewer_img.src = "/thumbnail/" + current_photo.id + ".jpg";
|
||
|
waiting_for_img = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
photo_viewer_img.src = "";
|
||
|
}
|
||
|
|
||
|
for (let index = 0; index < upcoming_imgs.length; index += 1)
|
||
|
{
|
||
|
upcoming_photo = photo_queue[index];
|
||
|
if (upcoming_photo !== undefined && upcoming_photo.has_thumbnail)
|
||
|
{
|
||
|
upcoming_imgs[index].src = "/thumbnail/" + upcoming_photo.id + ".jpg";
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
upcoming_imgs[index].src = "";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// UI - EVENT HANDLERS /////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
function onchange_action_select(event)
|
||
|
{
|
||
|
const select = event.target;
|
||
|
const button = select.closest("button");
|
||
|
const action = select.value;
|
||
|
const action_index = button.dataset.actionIndex;
|
||
|
action_map[action_index]["action"] = action;
|
||
|
|
||
|
const input = action_map[action_index]["tag_input"];
|
||
|
if (action === "add_tag" || action === "remove_tag")
|
||
|
{
|
||
|
input.classList.remove("hidden");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
action_map[action_index]["tag"] = null;
|
||
|
input.value = "";
|
||
|
input.classList.add("hidden");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onchange_action_tag(event)
|
||
|
{
|
||
|
const input = event.target;
|
||
|
const button = input.closest("button");
|
||
|
const action_index = button.dataset.actionIndex;
|
||
|
action_map[action_index]["tag"] = input.value;
|
||
|
}
|
||
|
|
||
|
function onclick_button(event)
|
||
|
{
|
||
|
if (waiting_for_img)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
const button = event.target;
|
||
|
const action_index = button.dataset.actionIndex;
|
||
|
const action = action_map[action_index]["action"];
|
||
|
const tag = action_map[action_index]["tag"];
|
||
|
process_current_photo(action, tag);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
function on_pageload()
|
||
|
{
|
||
|
hotkeys.register_hotkey("arrowleft", ()=>action_map["left"]["button"].click(), "Push the left button");
|
||
|
hotkeys.register_hotkey("arrowdown", ()=>action_map["down"]["button"].click(), "Push the middle button");
|
||
|
hotkeys.register_hotkey("arrowright", ()=>action_map["right"]["button"].click(), "Push the right button");
|
||
|
hotkeys.register_hotkey("arrowup", show_previous_photo, "Show the previous photo");
|
||
|
show_next_photo();
|
||
|
for (const button of document.getElementsByClassName("action_button"))
|
||
|
{
|
||
|
button.addEventListener("click", onclick_button);
|
||
|
}
|
||
|
for (const button of document.getElementsByClassName("action_select"))
|
||
|
{
|
||
|
button.addEventListener("change", onchange_action_select);
|
||
|
button.addEventListener("click", (event)=>{event.stopPropagation(); false});
|
||
|
}
|
||
|
for (const button of document.getElementsByClassName("action_tag_input"))
|
||
|
{
|
||
|
button.addEventListener("change", onchange_action_tag);
|
||
|
button.addEventListener("click", (event)=>{event.stopPropagation(); false});
|
||
|
}
|
||
|
}
|
||
|
document.addEventListener("DOMContentLoaded", on_pageload);
|
||
|
</script>
|
||
|
</html>
|