Change general design theme

This commit is contained in:
Jesús
2020-12-14 23:44:29 -05:00
parent 9d0be82e74
commit 7a765dc664
30 changed files with 5448 additions and 1526 deletions

View File

@@ -1,203 +1,238 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; media-src 'self' https://*.googlevideo.com; {{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}">
<title>{{ page_title }}</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; media-src 'self' https://*.googlevideo.com;
{{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}">
<link href="{{ theme_path }}" type="text/css" rel="stylesheet">
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
<link href="/youtube.com/static/comments.css" type="text/css" rel="stylesheet">
<link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon">
<link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml">
<style type="text/css">
{% block style %}
{{ style }}
{% endblock %}
</style>
<link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml"/>
<link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon"/>
<link href="/youtube.com/static/normalize.css" rel="stylesheet"/>
<link href="{{ theme_path }}" rel="stylesheet"/>
{% block style %}
{{ style }}
{% endblock %}
</head>
<body>
<header>
<a href="/youtube.com" id="home-link">Home</a>
<form id="site-search" action="/youtube.com/search">
<input type="search" name="query" class="search-box" value="{{ search_box_value }}"
{{ "autofocus" if request.path == "/" else "" }}>
<button type="submit" value="Search" class="search-button">Search</button>
<div class="dropdown">
<button class="dropdown-label">Options</button>
<div class="css-sucks">
<div class="dropdown-content">
<h3>Sort by</h3>
<input type="radio" id="sort_relevance" name="sort" value="0">
<label for="sort_relevance">Relevance</label>
<input type="radio" id="sort_upload_date" name="sort" value="2">
<label for="sort_upload_date">Upload date</label>
<input type="radio" id="sort_view_count" name="sort" value="3">
<label for="sort_view_count">View count</label>
<input type="radio" id="sort_rating" name="sort" value="1">
<label for="sort_rating">Rating</label>
<h3>Upload date</h3>
<input type="radio" id="time_any" name="time" value="0">
<label for="time_any">Any</label>
<input type="radio" id="time_last_hour" name="time" value="1">
<label for="time_last_hour">Last hour</label>
<input type="radio" id="time_today" name="time" value="2">
<label for="time_today">Today</label>
<input type="radio" id="time_this_week" name="time" value="3">
<label for="time_this_week">This week</label>
<input type="radio" id="time_this_month" name="time" value="4">
<label for="time_this_month">This month</label>
<input type="radio" id="time_this_year" name="time" value="5">
<label for="time_this_year">This year</label>
<h3>Type</h3>
<input type="radio" id="type_any" name="type" value="0">
<label for="type_any">Any</label>
<input type="radio" id="type_video" name="type" value="1">
<label for="type_video">Video</label>
<input type="radio" id="type_channel" name="type" value="2">
<label for="type_channel">Channel</label>
<input type="radio" id="type_playlist" name="type" value="3">
<label for="type_playlist">Playlist</label>
<input type="radio" id="type_movie" name="type" value="4">
<label for="type_movie">Movie</label>
<input type="radio" id="type_show" name="type" value="5">
<label for="type_show">Show</label>
<h3>Duration</h3>
<input type="radio" id="duration_any" name="duration" value="0">
<label for="duration_any">Any</label>
<input type="radio" id="duration_short" name="duration" value="1">
<label for="duration_short">Short (&lt; 4 minutes)</label>
<input type="radio" id="duration_long" name="duration" value="2">
<label for="duration_long">Long (&gt; 20 minutes)</label>
</div>
<header class="header">
<nav class="home">
<a href="/youtube.com" id="home-link">YouTube Local</a>
</nav>
<form class="form" id="site-search" action="/youtube.com/search">
<input type="search" name="query" class="search-box" value="{{ search_box_value }}"
{{ "autofocus" if request.path == "/" else "" }} placeholder="Type to search...">
<button type="submit" value="Search" class="search-button">Search</button>
<!-- options -->
<div class="dropdown">
<!-- hidden box -->
<input id="options-toggle-cbox" class="opt-box" role="button" type="checkbox">
<!-- end hidden box -->
<label class="dropdown-label" for="options-toggle-cbox">Options</label>
<div class="dropdown-content">
<h3>Sort by</h3>
<div class="option">
<input type="radio" id="sort_relevance" name="sort" value="0">
<label for="sort_relevance">Relevance</label>
</div>
<div class="option">
<input type="radio" id="sort_upload_date" name="sort" value="2">
<label for="sort_upload_date">Upload date</label>
</div>
<div class="option">
<input type="radio" id="sort_view_count" name="sort" value="3">
<label for="sort_view_count">View count</label>
</div>
<div class="option">
<input type="radio" id="sort_rating" name="sort" value="1">
<label for="sort_rating">Rating</label>
</div>
<h3>Upload date</h3>
<div class="option">
<input type="radio" id="time_any" name="time" value="0">
<label for="time_any">Any</label>
</div>
<div class="option">
<input type="radio" id="time_last_hour" name="time" value="1">
<label for="time_last_hour">Last hour</label>
</div>
<div class="option">
<input type="radio" id="time_today" name="time" value="2">
<label for="time_today">Today</label>
</div>
<div class="option">
<input type="radio" id="time_this_week" name="time" value="3">
<label for="time_this_week">This week</label>
</div>
<div class="option">
<input type="radio" id="time_this_month" name="time" value="4">
<label for="time_this_month">This month</label>
</div>
<div class="option">
<input type="radio" id="time_this_year" name="time" value="5">
<label for="time_this_year">This year</label>
</div>
<h3>Type</h3>
<div class="option">
<input type="radio" id="type_any" name="type" value="0">
<label for="type_any">Any</label>
</div>
<div class="option">
<input type="radio" id="type_video" name="type" value="1">
<label for="type_video">Video</label>
</div>
<div class="option">
<input type="radio" id="type_channel" name="type" value="2">
<label for="type_channel">Channel</label>
</div>
<div class="option">
<input type="radio" id="type_playlist" name="type" value="3">
<label for="type_playlist">Playlist</label>
</div>
<div class="option">
<input type="radio" id="type_movie" name="type" value="4">
<label for="type_movie">Movie</label>
</div>
<div class="option">
<input type="radio" id="type_show" name="type" value="5">
<label for="type_show">Show</label>
</div>
<h3>Duration</h3>
<div class="option">
<input type="radio" id="duration_any" name="duration" value="0">
<label for="duration_any">Any</label>
</div>
<div class="option">
<input type="radio" id="duration_short" name="duration" value="1">
<label for="duration_short">Short (&lt; 4 minutes)</label>
</div>
<div class="option">
<input type="radio" id="duration_long" name="duration" value="2">
<label for="duration_long">Long (&gt; 20 minutes)</label>
</div>
</div>
</div>
</form>
{% if header_playlist_names is defined %}
<form class="playlist" id="playlist-edit" action="/youtube.com/edit_playlist" method="post" target="_self">
<input class="play-box" name="playlist_name" id="playlist-name-selection" list="playlist-options" type="search" placeholder="I added your playlist...">
<datalist class="play-hidden" id="playlist-options">
{% for playlist_name in header_playlist_names %}
<option value="{{ playlist_name }}">{{ playlist_name }}</option>
{% endfor %}
</datalist>
<button class="play-add" type="submit" id="playlist-add-button" name="action" value="add">+List</button>
<div class="play-clean">
<button type="reset" id="item-selection-reset">Clear selection</button>
</div>
</form>
{% if header_playlist_names is defined %}
<form id="playlist-edit" action="/youtube.com/edit_playlist" method="post" target="_self">
<input name="playlist_name" id="playlist-name-selection" list="playlist-options" type="text">
<datalist id="playlist-options">
{% for playlist_name in header_playlist_names %}
<option value="{{ playlist_name }}">{{ playlist_name }}</option>
{% endfor %}
</datalist>
<button type="submit" id="playlist-add-button" name="action" value="add">Add to playlist</button>
<button type="reset" id="item-selection-reset">Clear selection</button>
</form>
<script>
/* Takes control of the form if javascript is enabled, so that adding stuff to a playlist will not cause things to stop loading, and will display a status message. If javascript is disabled, the form will still work using regular HTML methods, but causes things on the page (such as the video) to stop loading. */
var playlistAddForm = document.getElementById('playlist-edit');
(function main() {
/* Takes control of the form if javascript is enabled, so that adding stuff to a playlist will not cause things to stop loading, and will display a status message. If javascript is disabled, the form will still work using regular HTML methods, but causes things on the page (such as the video) to stop loading. */
const playlistAddForm = document.getElementById('playlist-edit');
function setStyle(element, property, value){
element.style[property] = value;
}
function removeMessage(messageBox){
messageBox.parentNode.removeChild(messageBox);
}
function setStyle(element, property, value){
element.style[property] = value;
}
function removeMessage(messageBox){
messageBox.parentNode.removeChild(messageBox);
}
function displayMessage(text, error=false){
let currentMessageBox = document.getElementById('message-box');
if(currentMessageBox !== null){
currentMessageBox.parentNode.removeChild(currentMessageBox);
}
let messageBox = document.createElement('div');
if(error){
messageBox.setAttribute('role', 'alert');
} else {
messageBox.setAttribute('role', 'status');
}
messageBox.setAttribute('id', 'message-box');
let textNode = document.createTextNode(text);
messageBox.appendChild(textNode);
document.querySelector('main').appendChild(messageBox);
let currentstyle = window.getComputedStyle(messageBox);
let removalDelay;
if(error){
removalDelay = 5000;
} else {
removalDelay = 1500;
}
window.setTimeout(setStyle, 20, messageBox, 'opacity', 1);
window.setTimeout(setStyle, removalDelay, messageBox, 'opacity', 0);
window.setTimeout(removeMessage, removalDelay+300, messageBox);
}
// https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Sending_forms_through_JavaScript
function sendData(event){
var clicked_button = document.activeElement;
if(clicked_button === null || clicked_button.getAttribute('type') !== 'submit' || clicked_button.parentElement != event.target){
console.log('ERROR: clicked_button not valid');
return;
}
if(clicked_button.getAttribute('value') !== 'add'){
return; // video(s) are being removed from playlist, just let it refresh the page
}
event.preventDefault();
var XHR = new XMLHttpRequest();
var FD = new FormData(playlistAddForm);
function displayMessage(text, error=false){
let currentMessageBox = document.getElementById('message-box');
if(currentMessageBox !== null){
currentMessageBox.parentNode.removeChild(currentMessageBox);
}
let messageBox = document.createElement('div');
if(error){
messageBox.setAttribute('role', 'alert');
} else {
messageBox.setAttribute('role', 'status');
}
messageBox.setAttribute('id', 'message-box');
let textNode = document.createTextNode(text);
messageBox.appendChild(textNode);
document.querySelector('main').appendChild(messageBox);
let currentstyle = window.getComputedStyle(messageBox);
let removalDelay;
if(error){
removalDelay = 5000;
} else {
removalDelay = 1500;
}
window.setTimeout(setStyle, 20, messageBox, 'opacity', 1);
window.setTimeout(setStyle, removalDelay, messageBox, 'opacity', 0);
window.setTimeout(removeMessage, removalDelay+300, messageBox);
}
// https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Sending_forms_through_JavaScript
function sendData(event){
var clicked_button = document.activeElement;
if(clicked_button === null || clicked_button.getAttribute('type') !== 'submit' || clicked_button.parentElement != event.target){
console.log('ERROR: clicked_button not valid');
return;
}
if(clicked_button.getAttribute('value') !== 'add'){
return; // video(s) are being removed from playlist, just let it refresh the page
}
event.preventDefault();
var XHR = new XMLHttpRequest();
var FD = new FormData(playlistAddForm);
if(FD.getAll('video_info_list').length === 0){
displayMessage('Error: No videos selected', true);
return;
}
if(FD.getAll('video_info_list').length === 0){
displayMessage('Error: No videos selected', true);
return;
}
if(FD.get('playlist_name') === ""){
displayMessage('Error: No playlist selected', true);
return;
}
if(FD.get('playlist_name') === ""){
displayMessage('Error: No playlist selected', true);
return;
}
// https://stackoverflow.com/questions/48322876/formdata-doesnt-include-value-of-buttons
FD.append('action', 'add');
// https://stackoverflow.com/questions/48322876/formdata-doesnt-include-value-of-buttons
FD.append('action', 'add');
XHR.addEventListener('load', function(event){
if(event.target.status == 204){
displayMessage('Added videos to playlist "' + FD.get('playlist_name') + '"');
} else {
displayMessage('Error adding videos to playlist: ' + event.target.status.toString(), true);
}
});
XHR.addEventListener('load', function(event){
if(event.target.status == 204){
displayMessage('Added videos to playlist "' + FD.get('playlist_name') + '"');
} else {
displayMessage('Error adding videos to playlist: ' + event.target.status.toString(), true);
}
});
XHR.addEventListener('error', function(event){
if(event.target.status == 0){
displayMessage('XHR failed: Check that XHR requests are allowed', true);
} else {
displayMessage('XHR failed: Unknown error', true);
}
});
XHR.addEventListener('error', function(event){
if(event.target.status == 0){
displayMessage('XHR failed: Check that XHR requests are allowed', true);
} else {
displayMessage('XHR failed: Unknown error', true);
}
});
XHR.open('POST', playlistAddForm.getAttribute('action'));
XHR.send(FD);
}
XHR.open('POST', playlistAddForm.getAttribute('action'));
XHR.send(FD);
}
playlistAddForm.addEventListener('submit', sendData);
playlistAddForm.addEventListener('submit', sendData);
}());
</script>
{% endif %}
</header>
<main>
{% block main %}
{{ main }}
{% endblock %}
<main class="main">
{% block main %}
{{ main }}
{% endblock %}
</main>
<footer class="footer">
<p>This site is Free/Libre Software</p>
<p>Current version: 3304bab @ master</p>
</footer>
</body>
</html>

View File

@@ -7,114 +7,30 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
main{
display:grid;
{% if current_tab == 'about' %}
grid-template-rows: 0fr 0fr 1fr;
grid-template-columns: 0fr 1fr;
{% else %}
grid-template-rows: repeat(5, 0fr);
grid-template-columns: auto 1fr;
{% endif %}
}
main .avatar{
grid-row:1;
grid-column:1;
height:200px;
width:200px;
}
main .summary{
grid-row:1;
grid-column:2;
margin-left: 5px;
}
.summary subscribe-unsubscribe, .summary short-description{
margin-top: 10px;
}
main .channel-tabs{
grid-row:2;
grid-column: 1 / span 2;
display:grid;
grid-auto-flow: column;
justify-content:start;
background-color: var(--interface-color);
padding: 3px;
padding-left: 6px;
}
#links-metadata{
display: grid;
grid-auto-flow: column;
grid-column-gap: 10px;
grid-column: 1/span 2;
justify-content: start;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 6px;
margin-bottom: 10px;
}
#number-of-results{
font-weight:bold;
}
.content{
grid-row: 4;
grid-column: 1 / span 2;
}
.search-content{
width: 800px;
margin-left: 10px;
}
.item-grid{
padding-left: 20px;
}
.item-list{
width:800px;
margin: auto;
}
.page-button-row{
margin-left: auto;
margin-right: auto;
}
.next-previous-button-row{
margin-left: auto;
margin-right: auto;
}
.tab{
padding: 5px 75px;
}
.channel-info{
grid-row: 3;
grid-column: 1 / span 3;
}
.channel-info ul{
padding-left: 40px;
}
.channel-info h3{
margin-left: 40px;
}
.channel-info .description{
white-space: pre-wrap;
min-width: 0;
margin-left: 40px;
}
.medium-item img{
max-width: 168px;
}
<link href="/youtube.com/static/channel.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<img class="avatar" src="{{ avatar }}">
<div class="summary">
<h2 class="title">{{ channel_name }}</h2>
<p class="short-description">{{ short_description }}</p>
<form method="POST" action="/youtube.com/subscriptions" class="subscribe-unsubscribe">
<input type="submit" value="{{ 'Unsubscribe' if subscribed else 'Subscribe' }}">
<input type="hidden" name="channel_id" value="{{ channel_id }}">
<input type="hidden" name="channel_name" value="{{ channel_name }}">
<input type="hidden" name="action" value="{{ 'unsubscribe' if subscribed else 'subscribe' }}">
</form>
<div class="author-container">
<div class="author">
<img alt="{{ channel_name }}" src="{{ avatar }}"/>
<h2>{{ channel_name }}</h2>
</div>
<div class="summary">
<p>{{ short_description }}</p>
</div>
<div class="subscribe">
<form method="POST" action="/youtube.com/subscriptions" class="subscribe-unsubscribe">
<input class="btn-subscribe" type="submit" value="{{ 'Unsubscribe' if subscribed else 'Subscribe' }}">
<input type="hidden" name="channel_id" value="{{ channel_id }}">
<input type="hidden" name="channel_name" value="{{ channel_name }}">
<input type="hidden" name="action" value="{{ 'unsubscribe' if subscribed else 'subscribe' }}">
</form>
</div>
</div>
<hr/>
<nav class="channel-tabs">
{% for tab_name in ('Videos', 'Playlists', 'About') %}
{% if tab_name.lower() == current_tab %}
@@ -153,8 +69,9 @@
</ul>
</div>
{% else %}
<div class="content {{ current_tab + '-content'}}">
<div id="links-metadata">
<!-- new-->
<div id="links-metadata">
{% if current_tab == 'videos' %}
{% set sorts = [('1', 'views'), ('2', 'oldest'), ('3', 'newest')] %}
<div id="number-of-results">{{ number_of_videos }} videos</div>
@@ -177,16 +94,18 @@
<a class="sort-button" href="{{ channel_url + '/' + current_tab + '?sort=' + sort_number }}">{{ 'Sort by ' + sort_name }}</a>
{% endif %}
{% endfor %}
</div>
</div>
<nav class="{{ 'item-list' if current_tab == 'search' else 'item-grid' }}">
<div class="video-container {{ current_tab + '-content'}}">
{% for item_info in items %}
{{ common_elements.item(item_info, include_author=false) }}
{% endfor %}
</nav>
</div>
<hr/>
<footer class="pagination-container">
{% if current_tab == 'videos' %}
<nav class="page-button-row">
<nav class="pagination-list">
{{ common_elements.page_buttons(number_of_pages, channel_url + '/' + current_tab, parameters_dictionary) }}
</nav>
{% elif current_tab == 'search' %}
@@ -194,6 +113,8 @@
{{ common_elements.next_previous_buttons(is_last_page, channel_url + '/' + current_tab, parameters_dictionary) }}
</nav>
{% endif %}
</div>
</footer>
<!-- /new-->
{% endif %}
{% endblock main %}

View File

@@ -5,23 +5,24 @@
<div class="comment">
<a class="author-avatar" href="{{ comment['author_url'] }}" title="{{ comment['author'] }}">
{% if include_avatar %}
<img class="author-avatar-img" src="{{ comment['author_avatar'] }}">
<img class="author-avatar-img" alt="{{ comment['author'] }}" src="{{ comment['author_avatar'] }}">
{% endif %}
</a>
<address>
<address class="author-name">
<a class="author" href="{{ comment['author_url'] }}" title="{{ comment['author'] }}">{{ comment['author'] }}</a>
</address>
<a class="permalink" href="{{ comment['permalink'] }}" title="permalink">
<time datetime="">{{ comment['time_published'] }}</time>
<time datetime="2012-01-14T15:23:44+02:00">{{ comment['time_published'] }}</time>
</a>
{% if timestamp_links %}
<span class="text">{{ common_elements.text_runs(comment['text'])|timestamps|safe }}</span>
<span class="comment-text">{{ common_elements.text_runs(comment['text'])|timestamps|safe }}</span>
{% else %}
<span class="text">{{ common_elements.text_runs(comment['text']) }}</span>
<span class="comment-text">{{ common_elements.text_runs(comment['text']) }}</span>
{% endif %}
<span class="likes">{{ comment['likes_text'] if comment['like_count'] else ''}}</span>
<div class="bottom-row">
<span class="comment-likes">{{ comment['likes_text'] if comment['like_count'] else ''}}</span>
<div class="button-row">
{% if settings.use_comments_js and comment['reply_count'] %}
<details class="replies" src="{{ comment['replies_url'] }}">
<summary>{{ comment['view_replies_text'] }}</summary>
@@ -33,7 +34,6 @@
{% endif %}
</div>
</div>
</div>
{% endmacro %}
@@ -57,8 +57,5 @@
<a class="page-button more-comments" href="{{ comments_info['more_comments_url'] }}">More comments</a>
{% endif %}
{% endif %}
{% endmacro %}

View File

@@ -3,16 +3,11 @@
{% if not slim %}
{% extends "base.html" %}
{% block style %}
.comments-area{
margin: auto;
width:640px;
}
<link href="/youtube.com/static/comments.css" rel="stylesheet">
{% endblock style %}
{% endif %}
{% block main %}
<section class="comments-area">
{% if not comments_info['is_replies'] %}
@@ -27,7 +22,6 @@
</section>
{% endif %}
{% if not comments_info['is_replies'] %}
<div class="comment-links">
{% for link_text, link_url in comments_info['comment_links'] %}
@@ -51,5 +45,3 @@
<script src="/youtube.com/static/js/comments.js"></script>
{% endif %}
{% endblock main %}

View File

@@ -15,60 +15,55 @@
{% endmacro %}
{% macro item(info, description=false, horizontal=true, include_author=true, include_badges=true, lazy_load=false) %}
<div class="item-box {{ info['type'] + '-item-box' }} {{'horizontal-item-box' if horizontal else 'vertical-item-box'}} {{'has-description' if description else 'no-description'}}">
<article class="item-box">
{% if info['error'] %}
{{ info['error'] }}
{% else %}
<div class="item {{ info['type'] + '-item' }}">
<div class="item-video {{ info['type'] + '-item' }}">
<a class="thumbnail-box" href="{{ info['url'] }}" title="{{ info['title'] }}">
{% if lazy_load %}
<img class="thumbnail-img lazy" data-src="{{ info['thumbnail'] }}">
{% else %}
<img class="thumbnail-img" src="{{ info['thumbnail'] }}">
{% endif %}
{% if info['type'] != 'channel' %}
<div class="thumbnail-info">
<span>{{ (info['video_count']|commatize + ' videos') if info['type'] == 'playlist' else info['duration'] }}</span>
</div>
{% endif %}
</a>
<div class="thumbnail {% if info['type'] == 'channel' %} channel {% endif %}">
{% if lazy_load %}
<img class="thumbnail-img lazy" alt="thumbnail" data-src="{{ info['thumbnail'] }}">
{% elif info['type'] == 'channel' %}
<img class="thumbnail-img channel" alt="thumbnail" src="{{ info['thumbnail'] }}">
{% else %}
<img class="thumbnail-img" alt="thumbnail" src="{{ info['thumbnail'] }}">
{% endif %}
<div class="title"><a class="title" href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a></div>
{% if info['type'] != 'channel' %}
<p class="length">{{ (info['video_count']|commatize + ' videos') if info['type'] == 'playlist' else info['duration'] }}</p>
{% endif %}
</div>
</a>
<h4 class="title"><a href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a></h4>
{% if include_author %}
{% if info.get('author_url') %}
<address title="{{ info['author'] }}">By <a href="{{ info['author_url'] }}">{{ info['author'] }}</a></address>
<address title="{{ info['author'] }}"><b><a href="{{ info['author_url'] }}">{{ info['author'] }}</a></b></address>
{% else %}
<address title="{{ info['author'] }}"><b>{{ info['author'] }}</b></address>
{% endif %}
{% endif %}
<ul class="stats {{'horizontal-stats' if horizontal else 'vertical-stats'}}">
<div class="stats {{'horizontal-stats' if horizontal else 'vertical-stats'}}">
{% if info['type'] == 'channel' %}
<li><span>{{ info['approx_subscriber_count'] }} subscribers</span></li>
<li><span>{{ info['video_count']|commatize }} videos</span></li>
<div>{{ info['approx_subscriber_count'] }} subscribers</div>
<div>{{ info['video_count']|commatize }} videos</div>
{% else %}
{% if info.get('approx_view_count') %}
<li><span class="views">{{ info['approx_view_count'] }} views</span></li>
{% endif %}
{% if info.get('time_published') %}
<li><time>{{ info['time_published'] }}</time></li>
<time>{{ info['time_published'] }}</time>
{% endif %}
{% if info.get('approx_view_count') %}
<div class="views">{{ info['approx_view_count'] }} views</div>
{% endif %}
{% endif %}
</ul>
{% if description %}
<span class="description">{{ text_runs(info.get('description', '')) }}</span>
{% endif %}
{% if include_badges %}
<span class="badges">{{ info['badges']|join(' | ') }}</span>
{% endif %}
</div>
</div>
{% if info['type'] == 'video' %}
<input class="item-checkbox" type="checkbox" name="video_info_list" value="{{ info['video_info'] }}" form="playlist-edit">
{% endif %}
{% endif %}
</div>
</article>
{% endmacro %}
{% macro page_buttons(estimated_pages, url, parameters_dictionary) %}
@@ -84,12 +79,12 @@
{% for page in range(page_start, page_end+1) %}
{% if page == current_page %}
<div class="page-button">{{ page }}</div>
<div class="page-link is-current">{{ page }}</div>
{% else %}
{# IMPORTANT: Jinja SUCKS #}
{# https://stackoverflow.com/questions/36886650/how-to-add-a-new-entry-into-a-dictionary-object-while-using-jinja2 #}
{% set _ = parameters_dictionary.__setitem__('page', page) %}
<a class="page-button" href="{{ url + '?' + parameters_dictionary|urlencode }}">{{ page }}</a>
<a class="page-link" href="{{ url + '?' + parameters_dictionary|urlencode }}">{{ page }}</a>
{% endif %}
{% endfor %}
@@ -101,11 +96,11 @@
{% if current_page != 1 %}
{% set _ = parameters_dictionary.__setitem__('page', current_page - 1) %}
<a class="page-button previous-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Previous page</a>
<a class="page-link previous-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Previous page</a>
{% endif %}
{% if not is_last_page %}
{% set _ = parameters_dictionary.__setitem__('page', current_page + 1) %}
<a class="page-button next-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Next page</a>
<a class="page-link next-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Next page</a>
{% endif %}
{% endmacro %}

View File

@@ -4,9 +4,13 @@
{% extends "base.html" %}
{% endif %}
{% block style %}
<link href="/youtube.com/static/home.css" rel="stylesheet">
{% endblock style %}
{% block main %}
{% if traceback %}
<div id="error-box">
<div class="code-error" id="error-box">
<h1>500 Uncaught exception:</h1>
<div class="code-box"><code>{{ traceback }}</code></div>
<p>Please report this issue at <a href="https://github.com/user234683/youtube-local/issues" target="_blank">https://github.com/user234683/youtube-local/issues</a></p>
@@ -16,4 +20,3 @@
<div id="error-message">{{ error_message }}</div>
{% endif %}
{% endblock %}

View File

@@ -1,16 +1,7 @@
{% set page_title = title %}
{% extends "base.html" %}
{% block style %}
ul {
background-color: var(--interface-color);
padding: 20px;
width: 400px;
margin: auto;
margin-top: 20px;
}
li {
margin-bottom: 10px;
}
<link href="/youtube.com/static/home.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<ul>

View File

@@ -2,41 +2,23 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
main > *{
width: 800px;
margin: auto;
}
.playlist-metadata{
display: flex;
flex-direction: row;
justify-content: space-between;
}
.playlist-title{
}
#playlist-remove-button{
align-self: center;
white-space: nowrap;
}
#results{
display: grid;
grid-auto-rows: 0fr;
grid-row-gap: 10px;
}
<link href="/youtube.com/static/local_playlist.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div class="playlist-metadata">
<h2 class="playlist-title">{{ playlist_name }}</h2>
<h2 class="play-title">{{ playlist_name }}</h2>
<input type="hidden" name="playlist_page" value="{{ playlist_name }}" form="playlist-edit">
<button type="submit" id="playlist-remove-button" name="action" value="remove" form="playlist-edit" formaction="">Remove from playlist</button>
<button class="play-action" type="submit" id="playlist-remove-button" name="action" value="remove" form="playlist-edit" formaction="">Remove from playlist</button>
</div>
<div id="results">
<div id="results" class="video-container">
{% for video_info in videos %}
{{ common_elements.item(video_info) }}
{% endfor %}
</div>
<nav class="page-button-row">
{{ common_elements.page_buttons(num_pages, '/https://www.youtube.com/playlists/' + playlist_name, parameters_dictionary) }}
</nav>
<footer class="pagination-container">
<nav class="pagination-list">
{{ common_elements.page_buttons(num_pages, '/https://www.youtube.com/playlists/' + playlist_name, parameters_dictionary) }}
</nav>
</footer>
{% endblock main %}

View File

@@ -2,20 +2,7 @@
{% extends "base.html" %}
{% block style %}
main{
display: flex;
justify-content: center;
}
ul{
background-color: var(--interface-color);
margin-top: 20px;
padding: 20px;
min-width: 400px;
align-self: start;
}
li{
margin-bottom: 10px;
}
<link href="/youtube.com/static/home.css" rel="stylesheet">
{% endblock style %}
{% block main %}
@@ -25,9 +12,3 @@ main{
{% endfor %}
</ul>
{% endblock main %}

View File

@@ -2,77 +2,39 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
main > * {
width: 800px;
margin:auto;
}
.playlist-metadata{
display:grid;
grid-template-columns: 0fr 1fr;
}
.playlist-thumbnail{
grid-row: 1 / span 5;
grid-column:1;
justify-self:start;
width:250px;
margin-right: 10px;
}
.playlist-title{
grid-row: 1;
grid-column:2;
}
.playlist-author{
grid-row:2;
grid-column:2;
}
.playlist-stats{
grid-row:3;
grid-column:2;
}
.playlist-description{
grid-row:4;
grid-column:2;
min-width:0px;
white-space: pre-line;
}
#results{
margin-top:10px;
display: grid;
grid-auto-rows: 0fr;
grid-row-gap: 10px;
}
<link href="/youtube.com/static/playlist.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div class="playlist-metadata">
<img class="playlist-thumbnail" src="{{ thumbnail }}">
<h2 class="playlist-title">{{ title }}</h2>
<a class="playlist-author" href="{{ author_url }}">{{ author }}</a>
<div class="author">
<img alt="{{ title }}" src="{{ thumbnail }}"/>
<h2>{{ title }}</h2>
</div>
<div class="summary">
<a class="playlist-author" href="{{ author_url }}">{{ author }}</a>
</div>
<div class="playlist-stats">
<div>{{ video_count|commatize }} videos</div>
<div>{{ view_count|commatize }} views</div>
<div>Last updated {{ time_published }}</div>
</div>
<div class="playlist-description">{{ common_elements.text_runs(description) }}</div>
</div>
<hr/>
<div id="results">
<div id="results" class="video-container">
{% for info in video_list %}
{{ common_elements.item(info) }}
{% endfor %}
</div>
<nav class="page-button-row">
{{ common_elements.page_buttons(num_pages, '/https://www.youtube.com/playlist', parameters_dictionary) }}
</nav>
<hr/>
<footer class="pagination-container">
<nav class="pagination-list">
{{ common_elements.page_buttons(num_pages, '/https://www.youtube.com/playlist', parameters_dictionary) }}
</nav>
</footer>
{% endblock main %}

View File

@@ -3,44 +3,31 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
main > * {
max-width: 800px;
margin: auto;
}
#result-info{
margin-top: 10px;
margin-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
}
#number-of-results{
font-weight:bold;
}
.item-list{
padding-left: 10px;
padding-right: 10px;
}
.badge{
background-color:#cccccc;
}
<link href="/youtube.com/static/search.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div id="result-info">
<div id="number-of-results">Approximately {{ '{:,}'.format(estimated_results) }} results ({{ '{:,}'.format(estimated_pages) }} pages)</div>
{% if corrections['type'] == 'showing_results_for' %}
<div>Showing results for <a>{{ common_elements.text_runs(corrections['corrected_query_text']) }}</a></div>
<div>Search instead for <a href="{{ corrections['original_query_url'] }}">{{ corrections['original_query_text'] }}</a></div>
{% elif corrections['type'] == 'did_you_mean' %}
<div>Did you mean <a href="{{ corrections['corrected_query_url'] }}">{{ common_elements.text_runs(corrections['corrected_query_text']) }}</a></div>
{% endif %}
</div>
<div class="item-list">
{% for info in results %}
{{ common_elements.item(info, description=true) }}
{% endfor %}
</div>
<nav class="page-button-row">
{{ common_elements.page_buttons(estimated_pages, '/https://www.youtube.com/search', parameters_dictionary) }}
</nav>
<div class="result-info" id="result-info">
<div id="number-of-results">Approximately {{ '{:,}'.format(estimated_results) }} results ({{ '{:,}'.format(estimated_pages) }} pages)</div>
{% if corrections['type'] == 'showing_results_for' %}
<div>Showing results for <a>{{ common_elements.text_runs(corrections['corrected_query_text']) }}</a></div>
<div>Search instead for <a href="{{ corrections['original_query_url'] }}">{{ corrections['original_query_text'] }}</a></div>
{% elif corrections['type'] == 'did_you_mean' %}
<div>Did you mean <a href="{{ corrections['corrected_query_url'] }}">{{ common_elements.text_runs(corrections['corrected_query_text']) }}</a></div>
{% endif %}
</div>
<!-- video item -->
<div class="video-container">
{% for info in results %}
{{ common_elements.item(info, description=true) }}
{% endfor %}
</div>
<hr/>
<!-- /video item -->
<footer class="pagination-container">
<nav class="pagination-list">
{{ common_elements.page_buttons(estimated_pages, '/https://www.youtube.com/search', parameters_dictionary) }}
</nav>
</footer>
{% endblock main %}

View File

@@ -1,28 +1,7 @@
{% set page_title = 'Settings' %}
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
.settings-form {
margin: auto;
width: 600px;
margin-top:10px;
padding: 10px;
display: block;
background-color: var(--interface-color);
}
.settings-list{
list-style: none;
padding: 0px;
}
.setting-item{
margin-bottom: 10px;
padding: 5px;
}
.setting-item label{
display: inline-block;
width: 250px;
}
<link href="/youtube.com/static/settings.css" rel="stylesheet">
{% endblock style %}
{% block main %}
@@ -30,37 +9,37 @@
{% for categ in categories %}
<h2>{{ categ|capitalize }}</h2>
<ul class="settings-list">
{% for setting_name, setting_info, value in settings_by_category[categ] %}
{% if not setting_info.get('hidden', false) %}
<li class="setting-item">
{% if 'label' is in(setting_info) %}
<label for="{{ 'setting_' + setting_name }}">{{ setting_info['label'] }}</label>
{% else %}
<label for="{{ 'setting_' + setting_name }}">{{ setting_name.replace('_', ' ')|capitalize }}</label>
{% endif %}
{% if setting_info['type'].__name__ == 'bool' %}
<input type="checkbox" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" {{ 'checked' if value else '' }}>
{% elif setting_info['type'].__name__ == 'int' %}
{% if 'options' is in(setting_info) %}
<select id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}">
{% for option in setting_info['options'] %}
<option value="{{ option[0] }}" {{ 'selected' if option[0] == value else '' }}>{{ option[1] }}</option>
{% endfor %}
</select>
{% for setting_name, setting_info, value in settings_by_category[categ] %}
{% if not setting_info.get('hidden', false) %}
<li class="setting-item">
{% if 'label' is in(setting_info) %}
<label for="{{ 'setting_' + setting_name }}">{{ setting_info['label'] }}</label>
{% else %}
<input type="number" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}" step="1">
<label for="{{ 'setting_' + setting_name }}">{{ setting_name.replace('_', ' ')|capitalize }}</label>
{% endif %}
{% elif setting_info['type'].__name__ == 'float' %}
{% elif setting_info['type'].__name__ == 'str' %}
<input type="text" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}">
{% else %}
<span>Error: Unknown setting type: setting_info['type'].__name__</span>
{% endif %}
</li>
{% endif %}
{% endfor %}
{% if setting_info['type'].__name__ == 'bool' %}
<input type="checkbox" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" {{ 'checked' if value else '' }}>
{% elif setting_info['type'].__name__ == 'int' %}
{% if 'options' is in(setting_info) %}
<select id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}">
{% for option in setting_info['options'] %}
<option value="{{ option[0] }}" {{ 'selected' if option[0] == value else '' }}>{{ option[1] }}</option>
{% endfor %}
</select>
{% else %}
<input type="number" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}" step="1">
{% endif %}
{% elif setting_info['type'].__name__ == 'float' %}
{% elif setting_info['type'].__name__ == 'str' %}
<input type="text" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}">
{% else %}
<span>Error: Unknown setting type: setting_info['type'].__name__</span>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
{% endfor %}
<input type="submit" value="Save settings">

View File

@@ -1,81 +1,7 @@
{% set page_title = 'Subscription Manager' %}
{% extends "base.html" %}
{% block style %}
.import-export{
display: flex;
flex-direction: row;
padding-left: 10px;
padding-top: 10px;
}
.subscriptions-import-form{
background-color: var(--interface-color);
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 300px;
padding:10px;
}
.subscriptions-import-form h2{
font-size: 1.25rem;
margin-bottom: 10px;
}
.import-submit-button{
margin-top:15px;
align-self: flex-end;
}
.subscriptions-export-links{
margin: 0px 0px 0px 20px;
background-color: var(--interface-color);
list-style: none;
max-width: 300px;
padding:10px;
}
.sub-list-controls{
background-color: var(--interface-color);
padding:10px;
}
.tag-group-list{
list-style: none;
margin-left: 10px;
margin-right: 10px;
padding: 0px;
}
.tag-group{
border-style: solid;
margin-bottom: 10px;
}
.sub-list{
list-style: none;
padding:10px;
column-width: 300px;
column-gap: 40px;
}
.sub-list-item{
display:flex;
margin-bottom: 10px;
break-inside:avoid;
}
.sub-list-item:not(.muted){
background-color: var(--interface-color);
}
.tag-list{
margin-left:15px;
font-weight:bold;
}
.sub-list-item-name{
margin-left:15px;
}
.sub-list-checkbox{
height: 1.5em;
min-width: 1.5em; // need min-width otherwise browser doesn't respect the width and squishes the checkbox down when there's too many tags
}
<link href="/youtube.com/static/subscription_manager.css" rel="stylesheet">
{% endblock style %}
@@ -89,8 +15,6 @@
{% endfor %}
{% endmacro %}
{% block main %}
<div class="import-export">
<form class="subscriptions-import-form" enctype="multipart/form-data" action="/youtube.com/import_subscriptions" method="POST">

View File

@@ -7,79 +7,21 @@
{% import "common_elements.html" as common_elements %}
{% block style %}
main{
display:flex;
flex-direction: row;
}
.video-section{
flex-grow: 1;
padding-left: 10px;
padding-top: 10px;
}
.current-tag{
margin-bottom:10px;
}
.video-section .page-button-row{
justify-content: center;
}
.subscriptions-sidebar{
flex-basis: 300px;
background-color: var(--interface-color);
border-left: 2px;
}
.sidebar-links{
display:flex;
justify-content: space-between;
padding-left:10px;
padding-right: 10px;
}
.sidebar-list{
list-style: none;
padding-left:10px;
padding-right: 10px;
}
.sidebar-list-item{
display:flex;
justify-content: space-between;
margin-bottom: 5px;
}
.sub-refresh-list .sidebar-item-name{
text-overflow: clip;
white-space: nowrap;
overflow: hidden;
max-width: 200px;
}
<link href="/youtube.com/static/subscription.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div class="video-section">
{% if current_tag %}
<h2 class="current-tag">{{ current_tag }}</h2>
{% endif %}
<nav class="item-grid">
{% for video_info in videos %}
{{ common_elements.item(video_info) }}
{% endfor %}
</nav>
<nav class="page-button-row">
{{ common_elements.page_buttons(num_pages, '/youtube.com/subscriptions', parameters_dictionary) }}
</nav>
</div>
<div class="subscriptions-sidebar">
<div class="sidebar-links">
<a href="/youtube.com/subscription_manager" class="sub-manager-link">Subscription Manager</a>
<form method="POST" class="refresh-all">
<a class="sidebar-title" href="/youtube.com/subscription_manager" class="sub-manager-link">Subscription Manager</a>
<form class="sidebar-action" method="POST" class="refresh-all">
<input type="submit" value="Check All">
<input type="hidden" name="action" value="refresh">
<input type="hidden" name="type" value="all">
</form>
</div>
<hr>
<ol class="sidebar-list tags">
{% if current_tag %}
<li class="sidebar-list-item">
@@ -105,7 +47,6 @@
</ol>
<hr>
<ol class="sidebar-list sub-refresh-list">
{% for subscription in subscription_list %}
<li class="sidebar-list-item {{ 'muted' if subscription['muted'] else '' }}">
@@ -119,7 +60,23 @@
</li>
{% endfor %}
</ol>
</div>
{% if current_tag %}
<h2 class="current-tag">{{ current_tag }}</h2>
{% endif %}
<div class="video-container">
{% for video_info in videos %}
{{ common_elements.item(video_info) }}
{% endfor %}
</div>
<hr/>
<footer class="pagination-container">
<nav class="pagination-list">
{{ common_elements.page_buttons(num_pages, '/youtube.com/subscriptions', parameters_dictionary) }}
</nav>
</footer>
{% endblock main %}

View File

@@ -3,378 +3,17 @@
{% import "common_elements.html" as common_elements %}
{% import "comments.html" as comments with context %}
{% block style %}
details > summary{
background-color: var(--interface-color);
border-style: outset;
border-width: 2px;
font-weight: bold;
padding-bottom: 2px;
}
details > summary:hover{
text-decoration: underline;
}
.playability-error{
height: 360px;
width: 640px;
grid-column: 2;
background-color: var(--video-background-color);
text-align:center;
}
.playability-error span{
position: relative;
top: 50%;
transform: translate(-50%, -50%);
}
.live-url-choices{
height: 360px;
width: 640px;
grid-column: 2;
background-color: var(--video-background-color);
padding: 25px 0px 0px 25px;
}
.live-url-choices ol{
list-style: none;
padding:0px;
margin:0px;
margin-top: 15px;
}
.live-url-choices input{
width: 400px;
}
.url-choice-label{
display: inline-block;
width: 150px;
}
{% if settings.theater_mode %}
/* This is the constant aspect ratio trick
Percentages in padding-top declarations are based on the width of the
parent element. We can use this trick to achieve a constant aspect ratio
for video-container-inner by setting height to 0.
So the video height will decrease if the browser window is narrow,
but it will keep same aspect ratio. Must use absolute positioning on
video to keep it inside its container since the container's height is 0.
However, because we widen the video player longer than the video's
intrinsic width for long video to make scrubbing easier, we can't use
the aspect ratio to set the height. The height needs to be the
intrinsic height in these cases. So we use a media query so aspect
ratio trick is only used if page width is less than or equal to
intrinsic video width.
*/
#video-container{
grid-column: 1 / span 5;
justify-self: center;
max-width: 100%;
width: {{ theater_video_target_width }}px;
margin-bottom: 10px;
}
#video-container-inner{
height: {{ video_height}}px;
position: relative;
}
@media(max-width:{{ video_width }}px){
#video-container-inner{
padding-top: calc(100%*{{video_height}}/{{video_width}});
height: 0px;
}
}
video{
background-color: var(--video-background-color);
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.side-videos{
grid-row: 2 /span 3;
width: 400px;
}
.video-info{
width: 640px;
}
{% else %}
#video-container{
grid-column: 2;
}
#video-container, #video-container-inner, video{
height: 360px;
width: 640px;
}
.side-videos{
grid-row: 1 /span 4;
}
{% endif %}
main{
display:grid;
grid-template-columns: 1fr 640px 40px 400px 1fr;
grid-template-rows: auto auto auto auto;
align-content: start;
}
.video-info{
grid-column: 2;
grid-row: 2;
display: grid;
grid-template-rows: 0fr 0fr 0fr 20px 0fr 0fr;
grid-template-columns: 1fr 1fr;
align-content: start;
}
.video-info > .title{
grid-column: 1 / span 2;
min-width: 0;
}
.video-info > .labels{
justify-self:start;
list-style: none;
padding: 0px;
margin: 5px 0px;
}
.video-info > .labels:empty{
margin: 0px;
}
.labels > li{
display: inline;
margin-right:5px;
background-color: var(--interface-color);
padding: 2px 5px;
border-style: solid;
border-width: 1px;
}
.video-info > address{
grid-column: 1;
grid-row: 3;
justify-self: start;
}
.video-info > .views{
grid-column: 2;
grid-row: 3;
justify-self:end;
}
.video-info > time{
grid-column: 1;
grid-row: 4;
justify-self:start;
}
.video-info > .likes-dislikes{
grid-column: 2;
grid-row: 4;
justify-self:end;
}
.video-info > .external-player-controls{
justify-self: start;
grid-row: 5;
grid-column: 1;
margin-bottom: 8px;
}
#speed-control{
width: 65px;
text-align: center;
background-color: var(--interface-color);
color: var(--text-color);
}
.video-info > .checkbox{
justify-self:end;
align-self: start;
grid-row: 5;
grid-column: 2;
}
.video-info > .download-dropdown{
grid-column:1 / span 2;
grid-row: 6;
}
.video-info > .description{
background-color:var(--interface-color);
margin-top:8px;
white-space: pre-wrap;
min-width: 0;
word-wrap: break-word;
grid-column: 1 / span 2;
grid-row: 7;
padding: 5px;
}
.music-list{
grid-row:8;
grid-column: 1 / span 2;
background-color: var(--interface-color);
padding-bottom: 7px;
}
.music-list table,th,td{
border: 1px solid;
}
.music-list th,td{
padding-left:4px;
padding-right:5px;
}
.music-list caption{
text-align:left;
font-weight:bold;
margin-bottom:5px;
}
.more-info{
grid-row: 9;
grid-column: 1 / span 2;
background-color: var(--interface-color);
}
.more-info > summary{
font-weight: normal;
border-width: 1px 0px;
border-style: solid;
}
.more-info-content{
padding: 5px;
}
.more-info-content p{
margin: 8px 0px;
}
.comments-area-outer{
grid-column: 2;
grid-row: 3;
margin-top:10px;
}
.comments-disabled{
background-color: var(--interface-color);
padding: 5px;
font-weight: bold;
}
.comments-area-inner{
padding-top: 10px;
}
.comment{
width:640px;
}
.side-videos{
grid-column: 4;
max-width: 640px;
}
#transcript-details{
margin-bottom: 10px;
}
table#transcript-table {
border-collapse: collapse;
width: 100%;
}
table#transcript-table td, th {
border: 1px solid #dddddd;
}
div#transcript-div {
background-color: var(--interface-color);
padding: 5px;
}
.playlist{
border-style: solid;
border-width: 2px;
border-color: lightgray;
margin-bottom: 10px;
}
.playlist-header{
background-color: var(--interface-color);
padding: 3px;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: lightgray;
}
.playlist-header h3{
margin: 2px;
}
.playlist-metadata{
list-style: none;
padding: 0px;
margin: 0px;
}
.playlist-metadata li{
display: inline;
margin: 2px;
}
.playlist-videos{
height: 300px;
overflow-y: scroll;
display: grid;
grid-auto-rows: 90px;
grid-row-gap: 10px;
padding-top: 10px;
}
.related-videos-inner{
padding-top: 10px;
display: grid;
grid-auto-rows: 90px;
grid-row-gap: 10px;
}
.thumbnail-box{ /* overides rule in shared.css */
height: 90px !important;
width: 120px !important;
}
/* Put related vids below videos when window is too small */
/* 1100px instead of 1080 because W3C is full of idiots who include scrollbar width */
@media (max-width:1100px){
main{
grid-template-columns: 1fr 640px 40px 1fr;
}
.side-videos{
margin-top: 10px;
grid-column: 2;
grid-row: 3;
width: initial;
}
.comments-area-outer{
grid-row: 4;
}
}
.download-dropdown-content{
background-color: var(--interface-color);
padding: 10px;
list-style: none;
margin: 0px;
}
li.download-format{
margin-bottom: 7px;
}
.format-attributes{
list-style: none;
padding: 0px;
margin: 0px;
display: flex;
flex-direction: row;
}
.format-attributes li{
white-space: nowrap;
max-height: 1.2em;
}
.format-ext{
width: 60px;
}
.format-video-quality{
width: 140px;
}
.format-audio-quality{
width: 120px;
}
.format-file-size{
width: 80px;
}
.format-codecs{
width: 120px;
}
<link href="/youtube.com/static/watch.css" rel="stylesheet">
{% endblock style %}
{% block main %}
{% if playability_error %}
<div class="playability-error">
<span>{{ 'Error: ' + playability_error }}
{% if invidious_reload_button %}
<a href="{{ video_url }}&use_invidious=0"><br>
Reload without invidious (for usage of new identity button).</a>
{% endif %}
{% if invidious_reload_button %}
<a href="{{ video_url }}&use_invidious=0"><br>
Reload without invidious (for usage of new identity button).</a>
{% endif %}
</span>
</div>
{% elif (video_sources.__len__() == 0 or live) and hls_formats.__len__() != 0 %}
@@ -387,143 +26,137 @@ Reload without invidious (for usage of new identity button).</a>
</ol>
</div>
{% else %}
<div id="video-container">
<div id="video-container-inner">
<video controls autofocus class="video" height="{{ video_height }}px">
{% for video_source in video_sources %}
<source src="{{ video_source['src'] }}" type="{{ video_source['type'] }}">
{% endfor %}
<figure class="sc-video">
<video id="js-video-player" playsinline controls>
{% for video_source in video_sources %}
<source src="{{ video_source['src'] }}" type="{{ video_source['type'] }}">
{% endfor %}
{% for source in subtitle_sources %}
{% if source['on'] %}
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
{% else %}
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
{% endif %}
{% endfor %}
{% for source in subtitle_sources %}
{% if source['on'] %}
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
{% else %}
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
{% endif %}
{% endfor %}
</video>
</figure>
</video>
</div>
</div>
{% if time_start != 0 %}
<script>
document.querySelector('video').currentTime = {{ time_start|tojson }};
document.querySelector('js-video-player').currentTime = {{ time_start|tojson }};
</script>
{% endif %}
{% endif %}
<div class="video-info">
<h2 class="title">{{ title }}</h2>
<ul class="labels">
{%- if unlisted -%}
<li class="is-unlisted">Unlisted</li>
{%- endif -%}
{%- if age_restricted -%}
<li class="age-restricted">Age-restricted</li>
{%- endif -%}
{%- if limited_state -%}
<li>Limited state</li>
{%- endif -%}
{%- if live -%}
<li>Live</li>
{%- endif -%}
</ul>
<address>Uploaded by <a href="{{ uploader_channel_url }}">{{ uploader }}</a></address>
<span class="views">{{ view_count }} views</span>
<div class="sc-info">
<div class="video-info">
<h1 class="v-title">{{ title }}</h1>
<time datetime="$upload_date">Published on {{ time_published }}</time>
<span class="likes-dislikes">{{ like_count }} likes {{ dislike_count }} dislikes</span>
<div class="external-player-controls">
<input id="speed-control" type="text">
<script>
var video = document.querySelector('video');
var speedInput = document.querySelector('#speed-control');
speedInput.addEventListener('keyup', (event) => {
if (event.key === 'Enter') {
var speed = parseFloat(speedInput.value);
if(!isNaN(speed)){
video.playbackRate = speed;
}
}
});
</script>
</div>
<input class="checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
<details class="download-dropdown">
<summary class="download-dropdown-label">Download</summary>
<ul class="download-dropdown-content">
{% for format in download_formats %}
<li class="download-format">
<a class="download-link" href="{{ format['url'] }}">
<ol class="format-attributes">
<li class="format-ext">{{ format['ext'] }}</li>
<li class="format-video-quality">{{ format['video_quality'] }}</li>
<li class="format-audio-quality">{{ format['audio_quality'] }}</li>
<li class="format-file-size">{{ format['file_size'] }}</li>
<li class="format-codecs">{{ format['codecs'] }}</li>
</ol>
</a>
</li>
{% endfor %}
{% for download in other_downloads %}
<li class="download-format">
<a href="{{ download['url'] }}">
<ol class="format-attributes">
<li class="format-ext">{{ download['ext'] }}</li>
<li class="format-label">{{ download['label'] }}</li>
</ol>
</a>
</li>
{% endfor %}
<ul class="labels">
{%- if unlisted -%}
<li class="is-unlisted">Unlisted</li>
{%- endif -%}
{%- if age_restricted -%}
<li class="age-restricted">Age-restricted</li>
{%- endif -%}
{%- if limited_state -%}
<li>Limited state</li>
{%- endif -%}
{%- if live -%}
<li>Live</li>
{%- endif -%}
</ul>
</details>
<address class="v-uploaded">Uploaded by <a href="{{ uploader_channel_url }}">{{ uploader }}</a></address>
<span class="v-views">{{ view_count }} views</span>
<time class="v-published" datetime="$upload_date">Published on {{ time_published }}</time>
<span class="v-likes-dislikes">{{ like_count }} likes {{ dislike_count }} dislikes</span>
<span class="description">{{ common_elements.text_runs(description)|escape|urlize|timestamps|safe }}</span>
<div class="music-list">
{% if music_list.__len__() != 0 %}
<hr>
<table>
<caption>Music</caption>
<tr>
{% for attribute in music_attributes %}
<th>{{ attribute }}</th>
{% endfor %}
</tr>
{% for track in music_list %}
<div class="external-player-controls">
<input class="speed" id="speed-control" type="text">
<script>
(function main() {
'use strict';
const video = document.getElementById('js-video-player');
const speedInput = document.getElementById('speed-control');
speedInput.addEventListener('keyup', (event) => {
if (event.key === 'Enter') {
let speed = parseFloat(speedInput.value);
if(!isNaN(speed)){
video.playbackRate = speed;
}
}
});
}());
</script>
</div>
<input class="v-checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
<details class="v-download">
<summary class="download-dropdown-label">Download</summary>
<ul class="download-dropdown-content">
{% for format in download_formats %}
<li class="download-format">
<a class="download-link" href="{{ format['url'] }}" download="{{ title }}.{{ format['ext'] }}">
{{ format['ext'] }} {{ format['video_quality'] }} {{ format['audio_quality'] }} {{ format['file_size'] }} {{ format['codecs'] }}
</a>
</li>
{% endfor %}
{% for download in other_downloads %}
<li class="download-format">
<a href="{{ download['url'] }}" download>
{{ download['ext'] }} {{ download['label'] }}
</a>
</li>
{% endfor %}
</ul>
</details>
<span class="v-description">{{ common_elements.text_runs(description)|escape|urlize|timestamps|safe }}</span>
<div class="v-music-list">
{% if music_list.__len__() != 0 %}
<hr>
<table>
<caption>Music</caption>
<tr>
{% for attribute in music_attributes %}
<td>{{ track.get(attribute.lower(), '') }}</td>
<th>{{ attribute }}</th>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
</div>
<details class="more-info">
<summary>More info</summary>
<div class="more-info-content">
<p>Tor exit node: {{ ip_address }}</p>
{% if invidious_used %}
<p>Used Invidious as fallback.</p>
{% endif %}
<p class="allowed-countries">Allowed countries: {{ allowed_countries|join(', ') }}</p>
{% if settings.use_sponsorblock_js %}
<ul class="more-actions">
<li><label><input type=checkbox id=skip_sponsors checked>skip sponsors</label> <span id=skip_n></span>
</ul>
{% for track in music_list %}
<tr>
{% for attribute in music_attributes %}
<td>{{ track.get(attribute.lower(), '') }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</details>
</div>
<details class="v-more-info">
<summary>More info</summary>
<div class="more-info-content">
<p>Tor exit node: {{ ip_address }}</p>
{% if invidious_used %}
<p>Used Invidious as fallback.</p>
{% endif %}
<p class="allowed-countries">Allowed countries: {{ allowed_countries|join(', ') }}</p>
{% if settings.use_sponsorblock_js %}
<ul class="more-actions">
<li><label><input type=checkbox id=skip_sponsors checked>skip sponsors</label> <span id=skip_n></span>
</ul>
{% endif %}
</div>
</details>
</div>
<div class="side-videos">
{% if playlist %}
<div class="playlist">
<div class="side-videos">
<!-- playlist -->
{% if playlist %}
<div class="site-playlist">
<div class="playlist-header">
<a href="{{ playlist['url'] }}" title="{{ playlist['title'] }}"><h3>{{ playlist['title'] }}</h3></a>
<ul class="playlist-metadata">
@@ -550,168 +183,172 @@ Reload without invidious (for usage of new identity button).</a>
</nav>
{% if playlist['current_index'] is not none %}
<script>
// from https://stackoverflow.com/a/6969486
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
var playability_error = {{ 'true' if playability_error else 'false' }};
var playlist_id = {{ playlist['id']|tojson }};
playlist_id = escapeRegExp(playlist_id);
(function main() {
// from https://stackoverflow.com/a/6969486
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
let playability_error = {{ 'true' if playability_error else 'false' }};
let playlist_id = {{ playlist['id']|tojson }};
playlist_id = escapeRegExp(playlist_id);
// read cookies on whether to autoplay thru playlist
// pain in the ass:
// https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
var cookieValue = document.cookie.replace(new RegExp(
'(?:(?:^|.*;\\s*)autoplay_' + playlist_id + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
var autoplayEnabled = 0;
if(cookieValue.length === 0){
autoplayEnabled = 0;
} else {
autoplayEnabled = Number(cookieValue);
}
// read cookies on whether to autoplay thru playlist
// pain in the ass:
// https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
let cookieValue = document.cookie.replace(new RegExp(
'(?:(?:^|.*;\\s*)autoplay_' + playlist_id + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
let autoplayEnabled = 0;
if(cookieValue.length === 0){
autoplayEnabled = 0;
} else {
autoplayEnabled = Number(cookieValue);
}
// check the checkbox if autoplay is on
var checkbox = document.querySelector('#autoplay-toggle');
if(autoplayEnabled){
checkbox.checked = true;
}
// check the checkbox if autoplay is on
let checkbox = document.querySelector('#autoplay-toggle');
if(autoplayEnabled){
checkbox.checked = true;
}
// listen for checkbox to turn autoplay on and off
checkbox.addEventListener( 'change', function() {
if(this.checked) {
autoplayEnabled = 1;
document.cookie = 'autoplay_' + playlist_id + '=1';
} else {
autoplayEnabled = 0;
document.cookie = 'autoplay_' + playlist_id + '=0';
}
});
// listen for checkbox to turn autoplay on and off
checkbox.addEventListener( 'change', function() {
if(this.checked) {
autoplayEnabled = 1;
document.cookie = 'autoplay_' + playlist_id + '=1';
} else {
autoplayEnabled = 0;
document.cookie = 'autoplay_' + playlist_id + '=0';
}
});
if(!playability_error){
// play the video if autoplay is on
var vid = document.querySelector('video');
if(autoplayEnabled){
vid.play();
}
}
const vid = document.getElementById('js-video-player');
if(!playability_error){
if(autoplayEnabled){
vid.play();
}
}
var currentIndex = {{ playlist['current_index']|tojson }};
{% if playlist['current_index']+1 == playlist['items']|length %}
var nextVideoUrl = null;
{% else %}
var nextVideoUrl = {{ (playlist['items'][playlist['current_index']+1]['url'])|tojson }};
{% endif %}
var nextVideoDelay = 1000;
let currentIndex = {{ playlist['current_index']|tojson }};
{% if playlist['current_index']+1 == playlist['items']|length %}
let nextVideoUrl = null;
{% else %}
let nextVideoUrl = {{ (playlist['items'][playlist['current_index']+1]['url'])|tojson }};
{% endif %}
let nextVideoDelay = 1000;
// scroll playlist to proper position
var pl = document.querySelector('.playlist-videos');
// item height + gap == 100
pl.scrollTop = 100*currentIndex;
// scroll playlist to proper position
let pl = document.querySelector('.playlist-videos');
// item height + gap == 100
pl.scrollTop = 100*currentIndex;
// go to next video when video ends
// https://stackoverflow.com/a/2880950
if(nextVideoUrl){
if(playability_error){
videoEnded();
} else {
vid.addEventListener('ended', videoEnded, false);
}
function nextVideo(){
if(autoplayEnabled){
window.location.href = nextVideoUrl;
}
}
function videoEnded(e) {
window.setTimeout(nextVideo, nextVideoDelay);
}
}
// go to next video when video ends
// https://stackoverflow.com/a/2880950
if(nextVideoUrl){
if(playability_error){
videoEnded();
} else {
vid.addEventListener('ended', videoEnded, false);
}
function nextVideo(){
if(autoplayEnabled){
window.location.href = nextVideoUrl;
}
}
function videoEnded(e) {
window.setTimeout(nextVideo, nextVideoDelay);
}
}
}());
</script>
{% endif %}
{% if playlist['id'] is not none %}
<script>
// lazy load playlist images
// copied almost verbatim from
// https://css-tricks.com/tips-for-rolling-your-own-lazy-loading/
// IntersectionObserver isn't supported in pre-quantum
// firefox versions, but the alternative of making it
// manually is a performance drain, so oh well
var observer = new IntersectionObserver(lazyLoad, {
(function main() {
// lazy load playlist images
// copied almost verbatim from
// https://css-tricks.com/tips-for-rolling-your-own-lazy-loading/
// IntersectionObserver isn't supported in pre-quantum
// firefox versions, but the alternative of making it
// manually is a performance drain, so oh well
let observer = new IntersectionObserver(lazyLoad, {
// where in relation to the edge of the viewport, we are observing
rootMargin: "100px",
// how much of the element needs to have intersected
// in order to fire our loading function
threshold: 1.0
});
// where in relation to the edge of the viewport, we are observing
rootMargin: "100px",
function lazyLoad(elements) {
elements.forEach(item => {
if (item.intersectionRatio > 0) {
// set the src attribute to trigger a load
item.target.src = item.target.dataset.src;
// stop observing this element. Our work here is done!
observer.unobserve(item.target);
};
});
};
// how much of the element needs to have intersected
// in order to fire our loading function
threshold: 1.0
});
function lazyLoad(elements) {
elements.forEach(item => {
if (item.intersectionRatio > 0) {
// set the src attribute to trigger a load
item.target.src = item.target.dataset.src;
// stop observing this element. Our work here is done!
observer.unobserve(item.target);
};
});
};
// Tell our observer to observe all img elements with a "lazy" class
var lazyImages = document.querySelectorAll('img.lazy');
lazyImages.forEach(img => {
observer.observe(img);
});
// Tell our observer to observe all img elements with a "lazy" class
let lazyImages = document.querySelectorAll('img.lazy');
lazyImages.forEach(img => {
observer.observe(img);
});
}());
</script>
{% endif %}
</div>
{% endif %}
{% endif %}
<!-- /playlist -->
{% if subtitle_sources %}
<details id="transcript-details">
<summary>Transcript</summary>
<div id="transcript-div">
<select id="select-tt">
{% for source in subtitle_sources %}
<option>{{ source['label'] }}</option>
{% if subtitle_sources %}
<details id="transcript-details">
<summary>Transcript</summary>
<div id="transcript-div">
<select id="select-tt">
{% for source in subtitle_sources %}
<option>{{ source['label'] }}</option>
{% endfor %}
</select>
<label for="transcript-use-table">Table view</label>
<input id="transcript-use-table" type="checkbox">
<table id="transcript-table"></table>
</div>
</details>
{% endif %}
{% if settings.related_videos_mode != 0 %}
<details class="related-videos-outer" {{'open' if settings.related_videos_mode == 1 else ''}}>
<summary>Related Videos</summary>
<nav class="related-videos-inner">
{% for info in related %}
{{ common_elements.item(info, include_badges=false) }}
{% endfor %}
</select>
<label for="transcript-use-table">Table view</label>
<input type="checkbox" id="transcript-use-table">
<table id="transcript-table"></table>
</div>
</details>
</nav>
</details>
{% endif %}
</div>
<!-- comments -->
{% if settings.comments_mode != 0 %}
{% if comments_disabled %}
<div class="comments-area-outer comments-disabled">Comments disabled</div>
{% else %}
<details class="comments-area-outer" {{'open' if settings.comments_mode == 1 else ''}}>
<summary>{{ comment_count|commatize }} comment{{'s' if comment_count != 1 else ''}}</summary>
<div class="comments-area-inner comments-area">
{% if comments_info %}
{{ comments.video_comments(comments_info) }}
{% endif %}
</div>
</details>
{% endif %}
{% endif %}
{% if settings.related_videos_mode != 0 %}
<details class="related-videos-outer" {{'open' if settings.related_videos_mode == 1 else ''}}>
<summary>Related Videos</summary>
<nav class="related-videos-inner">
{% for info in related %}
{{ common_elements.item(info, include_badges=false) }}
{% endfor %}
</nav>
</details>
{% endif %}
</div>
{% if settings.comments_mode != 0 %}
{% if comments_disabled %}
<div class="comments-area-outer comments-disabled">Comments disabled</div>
{% else %}
<details class="comments-area-outer" {{'open' if settings.comments_mode == 1 else ''}}>
<summary>{{ comment_count|commatize }} comment{{'s' if comment_count != 1 else ''}}</summary>
<section class="comments-area-inner comments-area">
{% if comments_info %}
{{ comments.video_comments(comments_info) }}
{% endif %}
</section>
</details>
{% endif %}
{% endif %}
<script> data = {{ js_data|tojson }} </script>
<script src="/youtube.com/static/js/common.js"></script>
<script src="/youtube.com/static/js/transcript-table.js"></script>