Ethan Dalool
65e0d58576
For channels with many thousands of videos, it can be a while before the DOM is fully parsed and ready to go. It is annoying to type in the box and find that it doesn't work yet, since the script tag comes after body. Why not move the script above body? Because I don't want to, and because you'd be filtering from an incomplete list of videos anyway.
564 lines
16 KiB
HTML
564 lines
16 KiB
HTML
<!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">
|
|
<script src="/static/js/common.js"></script>
|
|
<script src="/static/js/api.js"></script>
|
|
<script src="/static/js/spinner.js"></script>
|
|
|
|
<style>
|
|
#content_body
|
|
{
|
|
min-width: 200px;
|
|
width: 100%;
|
|
max-width: 1440px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
#content_body
|
|
{
|
|
display: grid;
|
|
grid-template:
|
|
"toolbox_left toolbox_right" auto
|
|
"video_cards video_cards" auto
|
|
/auto auto;
|
|
}
|
|
#toolbox_left
|
|
{
|
|
grid-area: toolbox_left;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
#toolbox_right
|
|
{
|
|
grid-area: toolbox_right;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
}
|
|
#video_cards
|
|
{
|
|
grid-area: 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;
|
|
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">
|
|
<div id="toolbox_left">
|
|
{% if channel is not none %}
|
|
<span><button class="refresh_button button_with_spinner" onclick="refresh_channel_form(false)">Refresh new videos</button></span>
|
|
<span><button class="refresh_button button_with_spinner" onclick="refresh_channel_form(true)">Refresh everything</button></span>
|
|
|
|
<p><!-- spacer --></p>
|
|
{% endif %}
|
|
|
|
<span>View
|
|
{% if channel is not none %}
|
|
<a class="merge_params" href="/channel/{{channel.id}}">All</a>
|
|
{% else %}
|
|
<a class="merge_params" href="/videos">All</a>
|
|
{% endif %}
|
|
{% for statename in all_states %}
|
|
{% if channel is not none %}
|
|
<a class="merge_params" href="/channel/{{channel.id}}/{{statename}}">{{statename.capitalize()}}</a>
|
|
{% else %}
|
|
<a class="merge_params" href="/videos/{{statename}}">{{statename.capitalize()}}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</span>
|
|
|
|
<p><!-- spacer --></p>
|
|
|
|
{% if channel is not none %}
|
|
<span>
|
|
New videos are:
|
|
<select onchange="set_automark_hook(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>
|
|
</span>
|
|
|
|
<p><!-- spacer --></p>
|
|
{% endif %}
|
|
|
|
<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>
|
|
</div>
|
|
|
|
<div id="toolbox_right">
|
|
{% if channel is not none %}
|
|
<button class="red_button button_with_confirm"
|
|
data-prompt="Delete channel and all videos?"
|
|
data-onclick="delete_channel_form();"
|
|
>Delete Channel</button>
|
|
{% endif %}
|
|
</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> items</center>
|
|
|
|
{% 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" 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_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, api.videos.mark_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, api.videos.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, api.videos.mark_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">
|
|
{% if channel is not none %}
|
|
var CHANNEL_ID = "{{channel.id}}";
|
|
{% endif %}
|
|
|
|
var DOWNLOAD_FILTER = "{{download_filter if download_filter else ""}}";
|
|
var video_card_first_selected = null;
|
|
var video_card_selections = [];
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
var search_filter_box = document.getElementById("search_filter");
|
|
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);
|
|
}
|
|
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 download_filter_class = "video_card_" + DOWNLOAD_FILTER;
|
|
Array.from(video_cards.getElementsByClassName("video_card")).forEach(function(video_card)
|
|
{
|
|
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);
|
|
}
|
|
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 = common.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 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.data.video_ids;
|
|
for (var index = 0; index < video_ids.length; index += 1)
|
|
{
|
|
var video_id = video_ids[index];
|
|
var state = response.data.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);
|
|
}
|
|
}
|
|
|
|
var set_automark_spinner = document.getElementById("set_automark_spinner");
|
|
set_automark_spinner = new spinner.Spinner(set_automark_spinner);
|
|
function set_automark_hook(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)
|
|
{
|
|
set_automark_spinner.hide();
|
|
}
|
|
}
|
|
</script>
|
|
</html>
|