<!DOCTYPE html5> <html> <head> {% import "header.html" as header %} <title>{{channel.name}}</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link rel="stylesheet" href="/static/common.css"> <script src="/static/common.js"></script> <style> #content_body { display: flex; flex-direction: column; min-width: 200px; width: 100%; max-width: 1440px; margin-left: auto; margin-right: auto; } .video_card { position: relative; display: grid; grid-template: "thumbnail details toolbox" auto "embed embed embed" auto /auto 1fr auto; grid-gap: 4px; margin: 8px; padding: 8px; border-radius: 4px; border: 1px solid #000; } .video_card:hover { box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25); } .video_card_pending { background-color: #ffffaa; } .video_card_ignored { background-color: #ffc886; } .video_card_selected { background-color: #13f4ff !important; } .video_card_downloaded { background-color: #aaffaa; } .video_thumbnail { grid-area: thumbnail; justify-self: center; } .video_details { grid-area: details; align-self: center; /* margin-right prevents the empty space of the <a> tag from swallowing click events meant for the video card. */ margin-right: auto; } .embed_toolbox { grid-area: embed; /* disabling pointer events on the toolbox prevents it from swallowing click events meant for the video card. Then we must re-enable them for child elements so the embed button is still clickable. This one uses pointer-events instead of margin because margin makes the whole embed too small. */ pointer-events: none; } .embed_toolbox * { pointer-events: auto; } .action_toolbox { grid-area: toolbox; justify-self: right; display: inline-flex; flex-direction: row; position: relative; margin-top: auto; margin-bottom: auto; } .video_action_dropdown { z-index: 1; background-color: #fff; padding: 4px; border: 1px solid #000; position: absolute; top: 100%; right: 0; display: none; flex-direction: column; } /* Thank you SimonSimCity https://stackoverflow.com/a/35153397 */ .video_iframe_holder { position: relative; width: 100%; height: 0; padding-bottom: 56.25%; } .video_iframe_holder iframe { position: absolute; width: 100%; height: 100%; left: 0; top: 0; } @media screen and (max-width: 600px) { .video_card { grid-template: "thumbnail" "details" "toolbox" "embed" /1fr; } } </style> </head> <body> {{header.make_header()}} <div id="content_body"> {% if channel is not none %} <span><button class="refresh_button" onclick="refresh_channel('{{channel.id}}', false, function(){location.reload()})">Refresh new videos</button></span> <span><button class="refresh_button" onclick="refresh_channel('{{channel.id}}', true, function(){location.reload()})">Refresh everything</button></span> <span><a class="merge_params" href="/channel/{{channel.id}}">All</a></span> {% else %} <span><a class="merge_params" href="/videos">All</a></span> {% endif %} {% for statename in all_states %} {% if channel is not none %} <span><a class="merge_params" href="/channel/{{channel.id}}/{{statename}}">{{statename}}</a></span> {% else %} <span><a class="merge_params" href="/videos/{{statename}}">{{statename}}</a></span> {% endif %} {% endfor %} <p><!-- spacer --></p> <span>Sort by <a class="merge_params" href="?orderby=published">Date</a>, <a class="merge_params" href="?orderby=duration">Duration</a>, <a class="merge_params" href="?orderby=views">Views</a>, <a class="merge_params" href="?orderby=random">Random</a> </span> <center><input type="text" id="search_filter"/></center> <center><span id="search_filter_count">{{videos|length}}</span> items</center> <div id="video_cards"> {% for video in videos %} <div id="video_card_{{video.id}}" data-ytid="{{video.id}}" onclick="onclick_select(event)" class="video_card video_card_{{video.download}}" > <img class="video_thumbnail" src="http://i3.ytimg.com/vi/{{video.id}}/default.jpg" height="100px"> <div class="video_details"> <a class="video_title" href="https://www.youtube.com/watch?v={{video.id}}">{{video._published_str}} - {{video.title}}</a> <span>({{video.duration | seconds_to_hms}})</span> <span>({{video.views}})</span> {% if channel is none %} <a href="/channel/{{video.author_id}}">({{video.author.name if video.author else video.author_id}})</a> {% endif %} </div> <div class="action_toolbox"> <button {% if video.download == "pending" %} class="video_action_pending hidden" {% else %} class="video_action_pending" {% endif %} onclick="action_button_passthrough(event, mark_video_state, 'pending')" >Revert to Pending</button> <button {% if video.download == "pending" %} class="video_action_download" {% else %} class="video_action_download hidden" {% endif %} onclick="action_button_passthrough(event, start_download)" >Download</button> <button {% if video.download == "pending" %} class="video_action_ignore" {% else %} class="video_action_ignore hidden" {% endif %} onclick="action_button_passthrough(event, mark_video_state, 'ignored')" >Ignore</button> </div> <div class="embed_toolbox"> <button class="show_embed_button" onclick="toggle_embed_video('{{video.id}}');">Embed</button> <button class="hide_embed_button hidden" onclick="toggle_embed_video('{{video.id}}');">Unembed</button> </div> </div> {% endfor %} </div> </div> </body> <script type="text/javascript"> var DOWNLOAD_FILTER = "{{download_filter if download_filter else ""}}"; var video_card_first_selected = null; var video_card_selections = []; var search_filter_box = document.getElementById("search_filter"); var search_filter_hook = function(event) { filter_video_cards(search_filter_box.value); } search_filter_box.addEventListener("keyup", search_filter_hook); function filter_video_cards(search_term) { /* Apply the current download filter (pending, ignored, downloaded) by removing mismatched cards from the dom. Apply the search filter textbox by hiding the mismatched cards. */ var count = 0; video_cards = document.getElementById("video_cards"); video_cards.classList.add("hidden"); search_term = search_term.toLocaleLowerCase(); var cards = video_cards.children; var download_filter_class = "video_card_" + DOWNLOAD_FILTER; for (var index = 0; index < video_cards.children.length; index += 1) { var video_card = video_cards.children[index]; var title = video_card.getElementsByClassName("video_title")[0].innerText.toLocaleLowerCase(); if (DOWNLOAD_FILTER && !video_card.classList.contains(download_filter_class)) { video_cards.removeChild(video_card); index -= 1; } else if (search_term !== "" && title.indexOf(search_term) == -1) { video_card.classList.add("hidden"); } else { video_card.classList.remove("hidden"); count += 1; } } video_cards.classList.remove("hidden"); document.getElementById("search_filter_count").innerText = count; } function toggle_embed_video(video_id) { var video_card = document.getElementById("video_card_" + video_id); var show_button = video_card.getElementsByClassName("show_embed_button")[0]; var hide_button = video_card.getElementsByClassName("hide_embed_button")[0]; var embed_toolbox = video_card.getElementsByClassName("embed_toolbox")[0]; var embeds = video_card.getElementsByClassName("video_iframe_holder"); if (embeds.length == 0) { var html = `<div class="video_iframe_holder"><iframe width="711" height="400" src="https://www.youtube.com/embed/${video_id}" frameborder="0" allow="encrypted-media" allowfullscreen></iframe></div>` var embed = html_to_element(html); embed_toolbox.appendChild(embed); show_button.classList.add("hidden"); hide_button.classList.remove("hidden"); } else { embeds[0].parentElement.removeChild(embeds[0]); show_button.classList.remove("hidden"); hide_button.classList.add("hidden"); } } function deselect_all() { var video_card_first_selected = null; for (var index = 0; index < video_card_selections.length; index +=1) { video_card_selections[index].classList.remove("video_card_selected"); } video_card_selections = []; } function html_to_element(html) { var template = document.createElement("template"); template.innerHTML = html; return template.content.firstChild; } function onclick_select(event) { if (!event.target.classList.contains("video_card")) { return; } if (video_card_first_selected === null) { video_card_first_selected = event.target; } var video_cards = Array.from(document.getElementById("video_cards").children); if (event.shiftKey === false && event.ctrlKey === false) { video_card_selections = []; video_card_selections.push(event.target); video_card_first_selected = event.target; } else if (event.shiftKey === true) { video_card_selections = []; var start_index = video_cards.indexOf(video_card_first_selected); var end_index = video_cards.indexOf(event.target); if (end_index < start_index) { var temp = start_index; start_index = end_index; end_index = temp; } for (var index = start_index; index <= end_index; index += 1) { if (video_cards[index].classList.contains("hidden")) { continue; } video_card_selections.push(video_cards[index]); } } else if (event.ctrlKey === true) { var existing_index = video_card_selections.indexOf(event.target) if (existing_index == -1) { video_card_first_selected = event.target; video_card_selections.push(event.target); } else { video_card_selections.splice(existing_index, 1); } } for (var index = 0; index < video_cards.length; index += 1) { card = video_cards[index]; if (video_card_selections.indexOf(card) > -1) { card.classList.add("video_card_selected"); } else { card.classList.remove("video_card_selected"); } } document.getSelection().removeAllRanges(); return false; } function action_button_passthrough(event, action_function, action_argument) { var elements; var this_card = event.target.parentElement.parentElement; if (video_card_selections.length > 0 && video_card_selections.indexOf(this_card) > -1) { elements = video_card_selections; } else { // Button -> button toolbox -> video card elements = [this_card]; } var video_ids = []; for (var index = 0; index < elements.length; index += 1) { video_ids.push(elements[index].dataset["ytid"]); } video_ids = video_ids.join(","); if (action_argument === undefined) { action_function(video_ids, receive_action_response); } else { action_function(video_ids, action_argument, receive_action_response); } if (! event.shiftKey) { deselect_all(); } } function give_action_buttons(video_card_div) { var toolbox = video_card_div.getElementsByClassName("action_toolbox")[0] var buttons = Array.from(toolbox.getElementsByTagName("button")); var is_pending = video_card_div.classList.contains("video_card_pending"); buttons.forEach(function(button) { if (is_pending) { button.classList.remove("hidden"); } else { button.classList.add("hidden"); } }); var button_pending = video_card_div.getElementsByClassName("video_action_pending")[0]; if (is_pending) { button_pending.classList.add("hidden"); } else { button_pending.classList.remove("hidden"); } } function receive_action_response(response) { var video_ids = response['video_ids']; for (var index = 0; index < video_ids.length; index += 1) { var video_id = video_ids[index]; var state = response['state']; var card = document.getElementById("video_card_" + video_id); {% for statename in all_states %} card.classList.remove("video_card_{{statename}}"); {% endfor %} card.classList.add("video_card_" + state); give_action_buttons(card); } } function refresh_channel(channel_id, force, callback) { var url = "/refresh_channel"; data = new FormData(); data.append("channel_id", channel_id); data.append("force", force) return common.post(url, data, callback); } function mark_video_state(video_ids, state, callback) { var url = "/mark_video_state"; data = new FormData(); data.append("video_ids", video_ids); data.append("state", state); return common.post(url, data, callback); } function start_download(video_ids, callback) { var url = "/start_download"; data = new FormData(); data.append("video_ids", video_ids); return common.post(url, data, callback); } </script> </html>