{% set page_title = title %} {% extends "base.html" %} {% 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 > .download-dropdown{ grid-column:1 / span 2; grid-row: 6; } .video-info > .checkbox{ justify-self:end; align-self: start; grid-row: 5; grid-column: 2; } .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; } {% 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 %} </span> </div> {% elif (video_sources.__len__() == 0 or live) and hls_formats.__len__() != 0 %} <div class="live-url-choices"> <span>Copy a url into your video player:</span> <ol> {% for fmt in hls_formats %} <li class="url-choice"><div class="url-choice-label">{{ fmt['video_quality'] }}: </div><input class="url-choice-copy" value="{{ fmt['url'] }}" readonly onclick="this.select();"></li> {% endfor %} </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 %} {% 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> </div> </div> {% if time_start != 0 %} <script> document.querySelector('video').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> <time datetime="$upload_date">Published on {{ time_published }}</time> <span class="likes-dislikes">{{ like_count }} likes {{ dislike_count }} dislikes</span> <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> </details> <input class="checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox"> <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 %} <tr> {% for attribute in music_attributes %} <td>{{ track.get(attribute.lower(), '') }}</td> {% 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> {% endif %} </div> </details> </div> <div class="side-videos"> {% if playlist %} <div class="playlist"> <div class="playlist-header"> <a href="{{ playlist['url'] }}" title="{{ playlist['title'] }}"><h3>{{ playlist['title'] }}</h3></a> <ul class="playlist-metadata"> <li>Autoplay: <input type="checkbox" id="autoplay-toggle"></li> {% if playlist['current_index'] is none %} <li>[Error!]/{{ playlist['video_count'] }}</li> {% else %} <li>{{ playlist['current_index']+1 }}/{{ playlist['video_count'] }}</li> {% endif %} <li><a href="{{ playlist['author_url'] }}" title="{{ playlist['author'] }}">{{ playlist['author'] }}</a></li> </ul> </div> <nav class="playlist-videos"> {% for info in playlist['items'] %} {# non-lazy load for 5 videos surrounding current video #} {# for non-js browsers or old such that IntersectionObserver doesn't work #} {# -10 is sentinel to not load anything if there's no current_index for some reason #} {% if (playlist.get('current_index', -10) - loop.index0)|abs is lt(5) %} {{ common_elements.item(info, include_badges=false, lazy_load=false) }} {% else %} {{ common_elements.item(info, include_badges=false, lazy_load=true) }} {% endif %} {% endfor %} </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); // 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); } // check the checkbox if autoplay is on var 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'; } }); if(!playability_error){ // play the video if autoplay is on var vid = document.querySelector('video'); 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; // scroll playlist to proper position var 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); } } </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, { // 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 }); 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); }); </script> {% endif %} </div> {% endif %} {% 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 type="checkbox" id="transcript-use-table"> <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 %} </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> {% if settings.use_video_hotkeys %} <script src="/youtube.com/static/js/hotkeys.js"></script> {% endif %} {% if settings.use_comments_js %} <script src="/youtube.com/static/js/comments.js"></script> {% endif %} {% if settings.use_sponsorblock_js %} <script src="/youtube.com/static/js/sponsorblock.js"></script> {% endif %} {% endblock main %}