<!DOCTYPE html> <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="icon" href="/favicon.png" type="image/png"/> <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/http.js"></script> <script src="/static/js/photo_clipboard.js"></script> <script src="/static/js/spinners.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>