<!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/css/common.css"> <link rel="stylesheet" href="/static/css/ycdl.css"> <script src="/static/js/common.js"></script> <script src="/static/js/api.js"></script> <script src="/static/js/hotkeys.js"></script> <script src="/static/js/spinner.js"></script> <style> .tabbed_container .tab { display: grid; grid-auto-flow: row; grid-gap: 8px; padding: 8px; } #video_cards { } .video_card { position: relative; display: grid; grid-template: "thumbnail details toolbox" auto "embed embed embed" auto /auto 1fr auto; grid-gap: 4px; margin: 8px 0; 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_title { word-break: break-word; } .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 toolbox" "embed embed" /auto 1fr; } } </style> </head> <body> {{header.make_header()}} <div id="content_body"> {% if channel is not none %} <h2>{{channel.name}}</h2> {% endif %} {% if channel is not none %} <div class="tabbed_container"> <div class="tab" data-tab-title="Videos"> <div><button class="refresh_button button_with_spinner" onclick="return refresh_channel_form(false);">Refresh new videos</button></div> <div><button class="refresh_button button_with_spinner" onclick="return refresh_channel_form(true);">Refresh everything</button></div> {% endif %} <div>View {% if channel is not none %} <a class="merge_params {{"bold" if not state else ""}}" href="/channel/{{channel.id}}">All</a> {% else %} <a class="merge_params {{"bold" if not state else ""}}" href="/videos">All</a> {% endif %} {% for statename in all_states %} {% if channel is not none %} <a class="merge_params {{"bold" if state == statename else ""}}" href="/channel/{{channel.id}}/{{statename}}">{{statename.capitalize()}}</a> {% else %} <a class="merge_params {{"bold" if state == statename else ""}}" href="/videos/{{statename}}">{{statename.capitalize()}}</a> {% endif %} {% endfor %} </div> <div>Sort by <a class="merge_params {{"bold" if orderby == "published" or not orderby else ""}}" href="?orderby=published">Date</a> <a class="merge_params {{"bold" if orderby == "duration" else ""}}" href="?orderby=duration">Duration</a> <a class="merge_params {{"bold" if orderby == "views" else ""}}" href="?orderby=views">Views</a> <a class="merge_params {{"bold" if orderby == "random" else ""}}" href="?orderby=random">Random</a> </div> <div id="video_cards"> <center><input disabled class="enable_on_pageload" type="text" id="search_filter"/></center> <center><span id="search_filter_count">{{videos|length}}</span> {{state or ""}} items</center> {% for video in videos %} <div id="video_card_{{video.id}}" data-ytid="{{video.id}}" onclick="return onclick_select(event);" class="video_card video_card_{{video.state}}" > <img class="video_thumbnail" loading="lazy" 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_string}} - {{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> <a href="/channel/{{video.author_id}}/pending">(p)</a> {% endif %} </div> <div class="action_toolbox"> <button {% if video.state == "pending" %} class="video_action_pending hidden" {% else %} class="video_action_pending" {% endif %} onclick="return action_button_passthrough(event, api.videos.mark_state, 'pending');" >Revert to Pending</button> {% if video.live_broadcast is none %} <button {% if video.state == "pending" %} class="video_action_download" {% else %} class="video_action_download hidden" {% endif %} onclick="return action_button_passthrough(event, api.videos.start_download);" >Download</button> {% else %} <button disabled>Video is {{video.live_broadcast}}</button> {% endif %} <button {% if video.state == "pending" %} class="video_action_ignore" {% else %} class="video_action_ignore hidden" {% endif %} onclick="return action_button_passthrough(event, api.videos.mark_state, 'ignored');" >Ignore</button> </div> <div class="embed_toolbox"> <button class="toggle_embed_button" onclick="return toggle_embed_video(event);">Embed</button> </div> </div> {% endfor %} </div> <!-- video_cards --> {% if channel is not none %} </div> <!-- tab-videos --> <div class="tab" data-tab-title="Settings"> <div> <label><input type="checkbox" id="set_autorefresh_checkbox" {{"checked" if channel.autorefresh else ""}} onchange="return set_autorefresh_form(event);"/> Automatically refresh this channel regularly.</label> <span id="set_autorefresh_spinner" class="hidden">Working...</span> </div> <div> New videos are: <select onchange="return set_automark_form(event);"> <option value="pending" {{"selected" if channel.automark == "pending" else ""}} >pending</option> <option value="downloaded" {{"selected" if channel.automark == "downloaded" else ""}} >downloaded</option> <option value="ignored" {{"selected" if channel.automark == "ignored" else ""}} >ignored</option> </select> <span id="set_automark_spinner" class="hidden">Working...</span> </div> <div> <input type="text" id="set_queuefile_extension_input" placeholder="Queuefile extension" value="{{channel.queuefile_extension or ''}}"/> <button id="set_queuefile_extension_button" class="button_with_spinner" onclick="return set_queuefile_extension_form();">Set extension</button> </div> <div> <input type="text" id="set_download_directory_input" placeholder="Queuefile directory" value="{{channel.download_directory or ''}}"/> <button id="set_download_directory_button" class="button_with_spinner" onclick="return set_download_directory_form();">Set directory</button> </div> <div><a href="https://www.youtube.com/channel/{{channel.id}}/videos">Channel page</a></div> <div><a href="https://www.youtube.com/feeds/videos.xml?channel_id={{channel.id}}">Channel RSS</a></div> <button class="red_button button_with_confirm" data-prompt="Delete channel and all videos?" data-onclick="return delete_channel_form();" >Delete Channel</button> </div> <!-- tab-settings --> </div> <!-- tabbed_container --> {% endif %} </div> </body> <script type="text/javascript"> const CHANNEL_ID = "{{channel.id if channel else ""}}"; const STATE = "{{state if state else ""}}"; // FILTER BOX ////////////////////////////////////////////////////////////////////////////////////// var search_filter_wait_for_typing; var search_filter_hook = function(event) { clearTimeout(search_filter_wait_for_typing); search_filter_wait_for_typing = setTimeout(function() { filter_video_cards(search_filter_box.value); }, 200); } 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. */ let count = 0; let video_cards = document.getElementById("video_cards"); video_cards.classList.add("hidden"); search_term = search_term.toLocaleLowerCase(); let state_class = "video_card_" + STATE; Array.from(video_cards.getElementsByClassName("video_card")).forEach(function(video_card) { let title = video_card.getElementsByClassName("video_title")[0].innerText.toLocaleLowerCase(); if (STATE && !video_card.classList.contains(state_class)) { video_cards.removeChild(video_card); } 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; } var search_filter_box = document.getElementById("search_filter"); search_filter_box.addEventListener("keyup", search_filter_hook); // VIDEO CARD SELECTION //////////////////////////////////////////////////////////////////////////// var video_card_selections = document.getElementsByClassName("video_card_selected"); var video_card_first_selected = null; function select_all() { video_card_first_selected = null; for (const video_card of document.getElementsByClassName("video_card")) { if (video_card.classList.contains("hidden")) { continue; } video_card.classList.add("video_card_selected"); } } function select_all_hotkey() { if (! CHANNEL_ID) { select_all(); return; } const tabbed_container = document.getElementsByClassName("tabbed_container")[0]; if (tabbed_container.dataset.activeTabId === "Videos") { select_all(); } } function deselect_all() { video_card_first_selected = null; for (const video_card of Array.from(video_card_selections)) { if (video_card.classList.contains("hidden")) { continue; } video_card.classList.remove("video_card_selected"); } } function deselect_all_hotkey() { if (! CHANNEL_ID) { deselect_all(); return; } const tabbed_container = document.getElementsByClassName("tabbed_container")[0]; if (tabbed_container.dataset.activeTabId === "Videos") { deselect_all(); } } function select_one(event) { deselect_all(); event.target.classList.add("video_card_selected"); video_card_first_selected = event.target; } function select_shift(event) { let video_cards = Array.from(document.getElementsByClassName("video_card")); video_cards = video_cards.filter(card => ! card.classList.contains("hidden")) let start_index = video_cards.indexOf(video_card_first_selected); let end_index = video_cards.indexOf(event.target); if (end_index < start_index) { [start_index, end_index] = [end_index, start_index]; } for (let index = start_index; index <= end_index; index += 1) { video_cards[index].classList.add("video_card_selected"); } } function select_ctrl(event) { if (event.target.classList.contains("video_card_selected")) { event.target.classList.remove("video_card_selected"); } else { video_card_first_selected = event.target; event.target.classList.add("video_card_selected"); } } function onclick_select(event) { if (!event.target.classList.contains("video_card")) { return; } if (video_card_first_selected === null) { video_card_first_selected = event.target; } if (event.shiftKey === false && event.ctrlKey === false) { select_one(event); } else if (event.shiftKey === true) { select_shift(event); } else if (event.ctrlKey === true) { select_ctrl(event); } document.getSelection().removeAllRanges(); return false; } // VIDEO CARD BUTTONS ////////////////////////////////////////////////////////////////////////////// function action_button_passthrough(event, action_function, action_argument) { let elements; const this_card = event.target.closest(".video_card"); if (this_card.classList.contains("video_card_selected")) { // The clicked card is indeed part of the current selected group. elements = Array.from(video_card_selections); } else { // The clicked card is actually not one of the selected, so we'll just // action it by itself. elements = [this_card]; } const video_ids = elements.map(element => element.dataset["ytid"]).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) { const toolbox = video_card_div.getElementsByClassName("action_toolbox")[0] const buttons = Array.from(toolbox.getElementsByTagName("button")); const is_pending = video_card_div.classList.contains("video_card_pending"); for (const button of buttons) { if (is_pending) { button.classList.remove("hidden"); } else { button.classList.add("hidden"); } } const 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) { if (response.meta.status !== 200) { alert(JSON.stringify(response)); return; } const video_ids = response.data.video_ids; const state = response.data.state; const state_class = "video_card_" + state; for (const video_id of video_ids) { const card = document.getElementById("video_card_" + video_id); {% for statename in all_states %} card.classList.remove("video_card_{{statename}}"); {% endfor %} card.classList.add(state_class); give_action_buttons(card); } } function toggle_embed_video(event) { const video_card = event.target.closest(".video_card"); const video_id = video_card.dataset.ytid; const toggle_button = video_card.getElementsByClassName("toggle_embed_button")[0]; const embed_toolbox = video_card.getElementsByClassName("embed_toolbox")[0]; const embeds = video_card.getElementsByClassName("video_iframe_holder"); if (embeds.length == 0) { const html = ` <div class="video_iframe_holder"> <iframe width="711" height="400" frameborder="0" allow="encrypted-media" allowfullscreen src="https://www.youtube.com/embed/${video_id}"></iframe> </div> ` const embed = common.html_to_element(html); embed_toolbox.appendChild(embed); toggle_button.innerText = "Unembed"; } else { embeds[0].parentElement.removeChild(embeds[0]); toggle_button.innerText = "Embed"; } } // CHANNEL ACTIONS ///////////////////////////////////////////////////////////////////////////////// function delete_channel_form() { api.channels.delete_channel(CHANNEL_ID, api.channels.callback_go_to_channels); } function refresh_channel_form(force) { console.log(`Refreshing channel ${CHANNEL_ID}, force=${force}.`); api.channels.refresh_channel(CHANNEL_ID, force, refresh_channel_callback) } function refresh_channel_callback(response) { if (response.meta.status == 200) { common.refresh(); } else { alert(JSON.stringify(response)); } } function set_automark_form(event) { set_automark_spinner.show(); api.channels.set_automark(CHANNEL_ID, event.target.value, set_automark_callback); } function set_automark_callback(response) { if (response.meta.status != 200) { alert(JSON.stringify(response)); } set_automark_spinner.hide(); } function set_autorefresh_form(event) { set_autorefresh_spinner.show(); api.channels.set_autorefresh(CHANNEL_ID, event.target.checked, set_autorefresh_callback); } function set_autorefresh_callback(response) { if (response.meta.status != 200) { alert(JSON.stringify(response)); } set_autorefresh_spinner.hide(); } function set_download_directory_form(event) { const download_directory = set_download_directory_input.value.trim(); api.channels.set_download_directory(CHANNEL_ID, download_directory, set_download_directory_callback); } function set_download_directory_callback(response) { if (response.meta.status != 200) { alert(JSON.stringify(response)); } window[set_download_directory_button.dataset.spinnerCloser](); } function set_queuefile_extension_form(event) { const extension = set_queuefile_extension_input.value.trim(); api.channels.set_queuefile_extension(CHANNEL_ID, extension, set_queuefile_extension_callback); } function set_queuefile_extension_callback(response) { if (response.meta.status != 200) { alert(JSON.stringify(response)); } window[set_queuefile_extension_button.dataset.spinnerCloser](); } if (CHANNEL_ID) { var set_queuefile_extension_input = document.getElementById("set_queuefile_extension_input"); var set_queuefile_extension_button = document.getElementById("set_queuefile_extension_button"); common.bind_box_to_button(set_queuefile_extension_input, set_queuefile_extension_button); var set_automark_spinner = document.getElementById("set_automark_spinner"); set_automark_spinner = new spinner.Spinner(set_automark_spinner); var set_autorefresh_spinner = document.getElementById("set_autorefresh_spinner"); set_autorefresh_spinner = new spinner.Spinner(set_autorefresh_spinner); } //////////////////////////////////////////////////////////////////////////////////////////////////// function on_pageload() { hotkeys.register_hotkey("ctrl a", select_all_hotkey, "Select all videos."); hotkeys.register_hotkey("ctrl d", deselect_all_hotkey, "Deselect all videos."); } document.addEventListener("DOMContentLoaded", on_pageload); </script> </html>