570 lines
17 KiB
HTML
570 lines
17 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">
|
|
<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> 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_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.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>
|
|
|
|
<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>
|
|
|
|
<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="show_embed_button" onclick="return toggle_embed_video('{{video.id}}');">Embed</button>
|
|
<button class="hide_embed_button hidden" onclick="return toggle_embed_video('{{video.id}}');">Unembed</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div> <!-- video_cards -->
|
|
|
|
{% if channel is not none %}
|
|
</div> <!-- tab-videos -->
|
|
|
|
<div class="tab" data-tab-title="Settings">
|
|
<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>
|
|
|
|
<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">
|
|
{% if channel is not none %}
|
|
const CHANNEL_ID = "{{channel.id}}";
|
|
{% endif %}
|
|
|
|
const STATE = "{{state if state else ""}}";
|
|
var video_card_first_selected = null;
|
|
var video_card_selections = document.getElementsByClassName("video_card_selected");
|
|
|
|
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.
|
|
*/
|
|
let count = 0;
|
|
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;
|
|
}
|
|
|
|
function toggle_embed_video(video_id)
|
|
{
|
|
let video_card = document.getElementById("video_card_" + video_id);
|
|
let show_button = video_card.getElementsByClassName("show_embed_button")[0];
|
|
let hide_button = video_card.getElementsByClassName("hide_embed_button")[0];
|
|
let embed_toolbox = video_card.getElementsByClassName("embed_toolbox")[0];
|
|
let embeds = video_card.getElementsByClassName("video_iframe_holder");
|
|
if (embeds.length == 0)
|
|
{
|
|
let 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>
|
|
`
|
|
let 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 select_all()
|
|
{
|
|
video_card_first_selected = null;
|
|
for (const video_card of document.getElementsByClassName("video_card"))
|
|
{
|
|
video_card.classList.add("video_card_selected");
|
|
}
|
|
}
|
|
|
|
function deselect_all()
|
|
{
|
|
video_card_first_selected = null;
|
|
for (const video_card of Array.from(video_card_selections))
|
|
{
|
|
video_card.classList.remove("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;
|
|
}
|
|
|
|
let video_cards = Array.from(document.getElementsByClassName("video_card"));
|
|
|
|
if (event.shiftKey === false && event.ctrlKey === false)
|
|
{
|
|
deselect_all();
|
|
event.target.classList.add("video_card_selected");
|
|
video_card_first_selected = event.target;
|
|
}
|
|
else if (event.shiftKey === true)
|
|
{
|
|
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)
|
|
{
|
|
if (video_cards[index].classList.contains("hidden"))
|
|
{
|
|
continue;
|
|
}
|
|
video_cards[index].classList.add("video_card_selected");
|
|
}
|
|
}
|
|
else if (event.ctrlKey === true)
|
|
{
|
|
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");
|
|
}
|
|
}
|
|
|
|
document.getSelection().removeAllRanges();
|
|
|
|
return false;
|
|
}
|
|
|
|
function action_button_passthrough(event, action_function, action_argument)
|
|
{
|
|
let elements;
|
|
let 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)
|
|
{
|
|
let toolbox = video_card_div.getElementsByClassName("action_toolbox")[0]
|
|
let buttons = Array.from(toolbox.getElementsByTagName("button"));
|
|
let 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"); }
|
|
});
|
|
|
|
let 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)
|
|
{
|
|
let video_ids = response.data.video_ids;
|
|
let state = response.data.state;
|
|
let state_class = "video_card_" + state;
|
|
for (const video_id of video_ids)
|
|
{
|
|
let 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);
|
|
}
|
|
}
|
|
|
|
var set_automark_spinner = document.getElementById("set_automark_spinner");
|
|
set_automark_spinner = new spinner.Spinner(set_automark_spinner);
|
|
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)
|
|
{
|
|
set_automark_spinner.hide();
|
|
}
|
|
}
|
|
|
|
set_queuefile_extension_input = document.getElementById("set_queuefile_extension_input");
|
|
set_queuefile_extension_button = document.getElementById("set_queuefile_extension_button");
|
|
common.bind_box_to_button(set_queuefile_extension_input, set_queuefile_extension_button);
|
|
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)
|
|
{
|
|
window[set_queuefile_extension_button.dataset.spinnerCloser]();
|
|
}
|
|
}
|
|
function on_pageload()
|
|
{
|
|
hotkeys.register_hotkey("ctrl a", select_all, "Select all videos.");
|
|
hotkeys.register_hotkey("ctrl d", deselect_all, "Deselect all videos.");
|
|
}
|
|
document.addEventListener("DOMContentLoaded", on_pageload);
|
|
</script>
|
|
</html>
|