mirror of
https://codeberg.org/simonrepp/hyper8.git
synced 2026-08-14 13:45:27 +02:00
Introduce custom video player
This commit is contained in:
@@ -27,3 +27,178 @@ if (copyLink) {
|
||||
|
||||
copyLink.addEventListener('click', event => copyToClipboard(event.target));
|
||||
}
|
||||
|
||||
const video = document.querySelector('video');
|
||||
|
||||
// TODO: Potentially move in separate script file that only gets loaded on video pages
|
||||
if (video) {
|
||||
let playbackUpdateInterval;
|
||||
|
||||
function formatTime(time) {
|
||||
const timeFloored = Math.floor(time);
|
||||
|
||||
if (timeFloored >= 3600) {
|
||||
const hours = Math.floor(timeFloored / 3600).toString();
|
||||
const minutes = Math.floor((timeFloored % 3600) / 60).toString().padStart(2, '0');
|
||||
const seconds = (timeFloored % 60).toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
} else if (timeFloored >= 60) {
|
||||
const minutes = Math.floor(timeFloored / 60).toString().padStart(2, '0');
|
||||
const seconds = (timeFloored % 60).toString().padStart(2, '0');
|
||||
return `${minutes}:${seconds}`;
|
||||
} else {
|
||||
const seconds = (timeFloored % 60).toString().padStart(2, '0');
|
||||
return `00:${seconds}`;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPlayButton = document.querySelector('#default_play');
|
||||
const intro = document.querySelector('.player_intro');
|
||||
const playerControls = document.querySelector('.player_controls');
|
||||
const playerWrapper = document.querySelector('.player_wrapper');
|
||||
const timelineInput = document.querySelector('.timeline input');
|
||||
const selectResolutionButton = document.querySelector('#select_resolution');
|
||||
const toggleFullscreenButton = document.querySelector('#toggle_fullscreen');
|
||||
const togglePlaybackButton = document.querySelector('#toggle_playback');
|
||||
const toggleSpeedButton = document.querySelector('#toggle_speed');
|
||||
const toggleVolumeButton = document.querySelector('#toggle_volume');
|
||||
|
||||
const persistedVolume = localStorage.getItem('hyper8Volume');
|
||||
|
||||
if (persistedVolume !== null) {
|
||||
video.volume = parseFloat(persistedVolume);
|
||||
|
||||
if (video.volume > 0.666) {
|
||||
toggleVolumeButton.replaceChildren(document.querySelector('#volume_loud_icon').content.cloneNode(true));
|
||||
} else if (video.volume < 0.333) {
|
||||
toggleVolumeButton.replaceChildren(document.querySelector('#volume_muted_icon').content.cloneNode(true));
|
||||
} else {
|
||||
toggleVolumeButton.replaceChildren(document.querySelector('#volume_medium_icon').content.cloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
function playbackUpdate() {
|
||||
const time = video.currentTime;
|
||||
const timeFormatted = formatTime(time);
|
||||
|
||||
timelineInput.value = time;
|
||||
timelineInput.setAttribute('aria-valuenow', time);
|
||||
timelineInput.setAttribute('aria-valuetext', timeFormatted);
|
||||
document.querySelector('.bar.progress').style.width = `${(time / video.duration) * 100}%`;
|
||||
document.querySelector('#time_current').innerHTML = timeFormatted;
|
||||
|
||||
if (playerControls.dataset.hideBy && (new Date()).getTime() > playerControls.dataset.hideBy) {
|
||||
playerControls.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function revealControls() {
|
||||
playerControls.dataset.hideBy = (new Date()).getTime() + 3000;
|
||||
playerControls.style.display = null;
|
||||
}
|
||||
|
||||
function selectResolution(event) {
|
||||
intro.style.display = 'none';
|
||||
|
||||
if (video.getAttribute('src') !== event.target.dataset.src) {
|
||||
video.src = event.target.dataset.src;
|
||||
selectResolutionButton.innerHTML = event.target.dataset.label;
|
||||
}
|
||||
|
||||
video.play();
|
||||
togglePlaybackButton.replaceChildren(document.querySelector('#pause_icon').content.cloneNode(true));
|
||||
playbackUpdateInterval = setInterval(playbackUpdate, 80);
|
||||
}
|
||||
|
||||
function togglePlayback() {
|
||||
if (video.paused) {
|
||||
intro.style.display = 'none';
|
||||
video.play();
|
||||
togglePlaybackButton.replaceChildren(document.querySelector('#pause_icon').content.cloneNode(true));
|
||||
playbackUpdateInterval = setInterval(playbackUpdate, 80);
|
||||
|
||||
} else {
|
||||
video.pause();
|
||||
clearInterval(playbackUpdateInterval);
|
||||
togglePlaybackButton.replaceChildren(document.querySelector('#play_icon').content.cloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of document.querySelectorAll('.versions_ring path[data-src]')) {
|
||||
path.addEventListener('click', selectResolution);
|
||||
}
|
||||
|
||||
defaultPlayButton.addEventListener('click', togglePlayback);
|
||||
|
||||
window.addEventListener('keydown', revealControls);
|
||||
playerWrapper.addEventListener('mousemove', revealControls);
|
||||
|
||||
selectResolutionButton.addEventListener('click', () => {
|
||||
intro.style.display = null;
|
||||
if (!video.paused) {
|
||||
video.pause();
|
||||
clearInterval(playbackUpdateInterval);
|
||||
togglePlaybackButton.replaceChildren(document.querySelector('#play_icon').content.cloneNode(true));
|
||||
}
|
||||
});
|
||||
|
||||
toggleFullscreenButton.addEventListener('click', () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
toggleFullscreenButton.replaceChildren(document.querySelector('#maximize_icon').content.cloneNode(true));
|
||||
} else {
|
||||
document.querySelector('.player_wrapper').requestFullscreen();
|
||||
toggleFullscreenButton.replaceChildren(document.querySelector('#minimize_icon').content.cloneNode(true));
|
||||
}
|
||||
});
|
||||
|
||||
toggleSpeedButton.addEventListener('click', () => {
|
||||
if (toggleSpeedButton.innerHTML === '1×') {
|
||||
video.playbackRate = 1.25;
|
||||
toggleSpeedButton.innerHTML = '1.25×'
|
||||
} else if (toggleSpeedButton.innerHTML === '1.25×') {
|
||||
video.playbackRate = 1.5;
|
||||
toggleSpeedButton.innerHTML = '1.5×'
|
||||
} else if (toggleSpeedButton.innerHTML === '1.5×') {
|
||||
video.playbackRate = 1.75;
|
||||
toggleSpeedButton.innerHTML = '1.75×'
|
||||
} else if (toggleSpeedButton.innerHTML === '1.75×') {
|
||||
video.playbackRate = 2;
|
||||
toggleSpeedButton.innerHTML = '2×'
|
||||
} else if (toggleSpeedButton.innerHTML === '2×') {
|
||||
video.playbackRate = 0.5;
|
||||
toggleSpeedButton.innerHTML = '0.5×'
|
||||
} else if (toggleSpeedButton.innerHTML === '0.5×') {
|
||||
video.playbackRate = 0.75;
|
||||
toggleSpeedButton.innerHTML = '0.75×'
|
||||
} else {
|
||||
video.playbackRate = 1;
|
||||
toggleSpeedButton.innerHTML = '1×'
|
||||
}
|
||||
});
|
||||
|
||||
toggleVolumeButton.addEventListener('click', () => {
|
||||
if (video.volume > 0.666) {
|
||||
video.volume = 0;
|
||||
toggleVolumeButton.replaceChildren(document.querySelector('#volume_muted_icon').content.cloneNode(true));
|
||||
localStorage.setItem('hyper8Volume', '0');
|
||||
} else if (video.volume < 0.333) {
|
||||
video.volume = 0.5;
|
||||
toggleVolumeButton.replaceChildren(document.querySelector('#volume_medium_icon').content.cloneNode(true));
|
||||
localStorage.setItem('hyper8Volume', '0.5');
|
||||
} else {
|
||||
video.volume = 1;
|
||||
toggleVolumeButton.replaceChildren(document.querySelector('#volume_loud_icon').content.cloneNode(true));
|
||||
localStorage.setItem('hyper8Volume', '1');
|
||||
}
|
||||
});
|
||||
|
||||
video.addEventListener('click', togglePlayback);
|
||||
togglePlaybackButton.addEventListener('click', togglePlayback);
|
||||
|
||||
timelineInput.addEventListener('input', _ => {
|
||||
intro.style.display = 'none';
|
||||
video.currentTime = timelineInput.value;
|
||||
playbackUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
+285
-180
@@ -1,300 +1,405 @@
|
||||
:root {
|
||||
--green: #5bd217;
|
||||
--letterbox: #000;
|
||||
--green: #5bd217;
|
||||
--letterbox: #000;
|
||||
}
|
||||
a {
|
||||
color: var(--fg-1);
|
||||
text-decoration: none;
|
||||
color: var(--fg-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
background-color: var(--bg-1);
|
||||
color: var(--fg-2);
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
background-color: var(--bg-1);
|
||||
color: var(--fg-2);
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
button {
|
||||
color: var(--fg-2);
|
||||
font-size: 1rem;
|
||||
color: var(--fg-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.17rem; }
|
||||
video {
|
||||
border-radius: .4rem;
|
||||
width: 100%;
|
||||
border-radius: .4rem;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.active { color: red; }
|
||||
.center {
|
||||
margin: 0 auto;
|
||||
max-width: 33rem;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
max-width: 33rem;
|
||||
position: relative;
|
||||
}
|
||||
.chart {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
column-gap: 1px;
|
||||
margin-bottom: .5rem;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
column-gap: 1px;
|
||||
margin-bottom: .5rem;
|
||||
position: relative;
|
||||
}
|
||||
.bar {
|
||||
height: 1.17rem;
|
||||
height: 1.17rem;
|
||||
}
|
||||
.chart .emissions {
|
||||
border: 1px solid var(--bg-mg);
|
||||
border-right: none;
|
||||
padding: 1px 0 1px 1px;
|
||||
border: 1px solid var(--bg-mg);
|
||||
border-right: none;
|
||||
padding: 1px 0 1px 1px;
|
||||
}
|
||||
.chart .savings {
|
||||
border: 1px solid var(--green);
|
||||
border-left: none;
|
||||
padding: 1px 1px 1px 0;
|
||||
border: 1px solid var(--green);
|
||||
border-left: none;
|
||||
padding: 1px 1px 1px 0;
|
||||
}
|
||||
.chart .label {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--fg-1);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
#copy-link {
|
||||
align-items: center;
|
||||
background: var(--bg-2);
|
||||
border: none;
|
||||
border-radius: .2rem;
|
||||
color: var(--fg-1);
|
||||
column-gap: .3rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: .2rem .5rem .2rem .4rem;
|
||||
align-items: center;
|
||||
background: var(--bg-2);
|
||||
border: none;
|
||||
border-radius: .2rem;
|
||||
color: var(--fg-1);
|
||||
column-gap: .3rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: .2rem .5rem .2rem .4rem;
|
||||
}
|
||||
#copy-link::after {
|
||||
color: var(--fg-2);
|
||||
padding-left: .5rem;
|
||||
color: var(--fg-2);
|
||||
padding-left: .5rem;
|
||||
}
|
||||
#copy-link.confirmed::after { content: var(--t-copied); }
|
||||
#copy-link.failed::after { content: var(--t-failed); }
|
||||
#copy-link:focus-visible,
|
||||
#copy-link:hover {
|
||||
background-color: var(--fg-1);
|
||||
color: var(--bg-1);
|
||||
background-color: var(--fg-1);
|
||||
color: var(--bg-1);
|
||||
}
|
||||
.current { background-color: var(--bg-3); }
|
||||
.emissions .bar { background-color: var(--bg-mg); }
|
||||
footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 3rem auto 0 auto;
|
||||
padding: 1rem 0;
|
||||
width: min(33rem, calc(100vw - 2rem));
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 3rem auto 0 auto;
|
||||
padding: 1rem 0;
|
||||
width: min(33rem, calc(100vw - 2rem));
|
||||
}
|
||||
header {
|
||||
align-items: center;
|
||||
column-gap: .7rem;
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
padding: .5rem 0;
|
||||
align-items: center;
|
||||
column-gap: .7rem;
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
padding: .5rem 0;
|
||||
}
|
||||
header a {
|
||||
border-bottom: .2rem solid transparent;
|
||||
color: var(--fg-1);
|
||||
outline: none;
|
||||
padding: .3rem 0;
|
||||
border-bottom: .2rem solid transparent;
|
||||
color: var(--fg-1);
|
||||
outline: none;
|
||||
padding: .3rem 0;
|
||||
}
|
||||
header a:focus-visible,
|
||||
header a:hover {
|
||||
border-bottom-color: var(--fg-1);
|
||||
border-bottom-color: var(--fg-1);
|
||||
}
|
||||
img { max-width: 100%; }
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
}
|
||||
.layout .growing { flex-grow: 1; }
|
||||
.logo {
|
||||
background-color: var(--fg-2);
|
||||
color: var(--bg-1);
|
||||
font-weight: 500;
|
||||
padding: .5rem;
|
||||
background-color: var(--fg-2);
|
||||
color: var(--bg-1);
|
||||
font-weight: 500;
|
||||
padding: .5rem;
|
||||
}
|
||||
.player_controls {
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
.player_controls button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: .6rem;
|
||||
}
|
||||
.player_controls button svg {
|
||||
height: 1.4rem;
|
||||
width: 1.4rem;
|
||||
}
|
||||
.player_controls .control_pane {
|
||||
background-color: var(--bg-1-overlay);
|
||||
display: flex;
|
||||
font-size: .85rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.player_controls .control_pane button { font-size: .85rem; }
|
||||
.player_controls .control_pane div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.player_controls .control_pane input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
.player_controls .timeline {
|
||||
height: .8rem;
|
||||
position: relative;
|
||||
}
|
||||
.player_controls .timeline * { cursor: pointer; }
|
||||
.player_controls .timeline .bar {
|
||||
bottom: 0;
|
||||
height: .3rem;
|
||||
position: absolute;
|
||||
}
|
||||
.player_controls .timeline:hover .bar { height: .35rem; }
|
||||
.player_controls .timeline .bar.backdrop {
|
||||
background-color: var(--bg-mg);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.player_controls .timeline .bar.progress {
|
||||
background-color: var(--fg-1);
|
||||
width: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.player_controls .timeline input {
|
||||
appearance: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
.player_intro {
|
||||
align-items: center;
|
||||
background-color: var(--bg-1-overlay);
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.player_intro #default_play {
|
||||
background: none;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.player_intro #default_play svg {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
.player_intro .versions_ring {
|
||||
position: absolute;
|
||||
height: 20rem;
|
||||
width: 20rem;
|
||||
}
|
||||
.player_intro .versions_ring path {
|
||||
cursor: pointer;
|
||||
fill: var(--fg-3);
|
||||
}
|
||||
.player_intro .versions_ring path:focus-visible,
|
||||
.player_intro .versions_ring path:hover {
|
||||
fill: var(--fg-1);
|
||||
outline: none;
|
||||
}
|
||||
.player_intro .versions_ring text { font-size: .9rem; }
|
||||
.player_intro .versions_ring text.size { fill: var(--fg-1); }
|
||||
.player_wrapper { position: relative; }
|
||||
.player_wrapper button { cursor: pointer; }
|
||||
.player_wrapper button:hover { color: var(--fg-1); }
|
||||
.playlist > * {
|
||||
column-gap: 1rem;
|
||||
display: flex;
|
||||
outline: none;
|
||||
column-gap: 1rem;
|
||||
display: flex;
|
||||
outline: none;
|
||||
}
|
||||
.playlist > *:focus-visible,
|
||||
.playlist > *:hover { background-color: var(--bg-2); }
|
||||
.playlist .description {
|
||||
align-items: center;
|
||||
color: var(--fg-1);
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
color: var(--fg-1);
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.playlist_context {
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: .4rem;
|
||||
/* Shadows only in light mode or different strategy between both color schemes */
|
||||
box-shadow: 0 0 12rem #0001;
|
||||
border: 1px solid var(--bg-2);
|
||||
border-radius: .4rem;
|
||||
/* TODO: Shadows only in light mode or different strategy between both color schemes */
|
||||
box-shadow: 0 0 12rem #0001;
|
||||
}
|
||||
.playlist_context > a {
|
||||
display: flex;
|
||||
outline: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
.playlist_context > a:first-child {
|
||||
border-top-left-radius: .4rem;
|
||||
border-top-right-radius: .4rem;
|
||||
border-top-left-radius: .4rem;
|
||||
border-top-right-radius: .4rem;
|
||||
}
|
||||
.playlist_context > a:first-child .thumbnail { border-top-left-radius: .4rem }
|
||||
.playlist_context > a:last-child {
|
||||
border-bottom-left-radius: .4rem;
|
||||
border-bottom-right-radius: .4rem;
|
||||
border-bottom-left-radius: .4rem;
|
||||
border-bottom-right-radius: .4rem;
|
||||
}
|
||||
.playlist_context > a:last-child .thumbnail { border-bottom-left-radius: .4rem }
|
||||
.playlist_context > a:focus-visible,
|
||||
.playlist_context > a:hover { background-color: var(--bg-2); }
|
||||
.playlist_context > a:not(:last-child) {
|
||||
border-bottom: 1px solid var(--bg-1);
|
||||
border-bottom: 1px solid var(--bg-1);
|
||||
}
|
||||
.playlist_context .current { background-color: var(--bg-3); }
|
||||
.playlist_context .description {
|
||||
align-items: flex-start;
|
||||
color: var(--fg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
padding: .2rem .8rem;
|
||||
align-items: flex-start;
|
||||
color: var(--fg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
padding: .2rem .8rem;
|
||||
}
|
||||
.playlist_context .details { font-size: .8rem; }
|
||||
.playlist_context .duration { color: var(--fg-3); }
|
||||
.playlist_context .label {
|
||||
font-size: .9rem;
|
||||
max-width: 15rem; /* TODO: Resolve this hack, need dynamic fitting into max-width */
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: .9rem;
|
||||
max-width: 15rem; /* TODO: Resolve this hack, need dynamic fitting into max-width */
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.playlist_context .more {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.playlist_context .number {
|
||||
background-color: var(--bg-1);
|
||||
border-bottom-left-radius: .2rem;
|
||||
color: var(--fg-3);
|
||||
font-size: .8rem;
|
||||
padding: 0 .4rem 0 .5rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-color: var(--bg-1);
|
||||
border-bottom-left-radius: .2rem;
|
||||
color: var(--fg-3);
|
||||
font-size: .8rem;
|
||||
padding: 0 .4rem 0 .5rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.playlist_context .thumbnail {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
height: 4rem;
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
height: 4rem;
|
||||
}
|
||||
.playlist_header {
|
||||
background-color: var(--bg-1);
|
||||
padding: .8rem;
|
||||
background-color: var(--bg-1);
|
||||
padding: .8rem;
|
||||
}
|
||||
.release_date { color: var(--fg-3); }
|
||||
.savings .bar { background-color: var(--green); }
|
||||
.savings_percent {
|
||||
color: var(--green);
|
||||
position: absolute;
|
||||
left: calc(100% + .5rem);
|
||||
color: var(--green);
|
||||
position: absolute;
|
||||
left: calc(100% + .5rem);
|
||||
}
|
||||
.size {
|
||||
color: var(--bg-mg);
|
||||
position: absolute;
|
||||
right: calc(100% + .5rem);
|
||||
color: var(--bg-mg);
|
||||
position: absolute;
|
||||
right: calc(100% + .5rem);
|
||||
}
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.site_title {
|
||||
color: var(--fg-2);
|
||||
font-size: 1.17rem;
|
||||
color: var(--fg-2);
|
||||
font-size: 1.17rem;
|
||||
}
|
||||
.split {
|
||||
display: flex;
|
||||
row-gap: 1rem;
|
||||
display: flex;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
svg {
|
||||
fill: currentColor;
|
||||
display: block;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
fill: currentColor;
|
||||
display: block;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
.system_info {
|
||||
align-items: center;
|
||||
color: var(--fg-3);
|
||||
column-gap: .4rem;
|
||||
display: flex;
|
||||
font-size: .8rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: .3rem;
|
||||
align-items: center;
|
||||
color: var(--fg-3);
|
||||
column-gap: .4rem;
|
||||
display: flex;
|
||||
font-size: .8rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: .3rem;
|
||||
}
|
||||
.thumbnail {
|
||||
aspect-ratio: var(--poster-aspect);
|
||||
background-color: var(--letterbox);
|
||||
border-radius: 0.4rem;
|
||||
flex-shrink: 0;
|
||||
height: 9rem;
|
||||
object-fit: contain;
|
||||
aspect-ratio: var(--poster-aspect);
|
||||
background-color: var(--letterbox);
|
||||
border-radius: 0.4rem;
|
||||
flex-shrink: 0;
|
||||
height: 9rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
.thumbnail_wrapper { position: relative; }
|
||||
.thumbnail_wrapper .duration {
|
||||
background-color: hsla(0, 0%, 0%, .9);
|
||||
border-radius: .2rem;
|
||||
bottom: calc(var(--poster-aspect) * 5%);
|
||||
color: #fff;
|
||||
font-size: .8rem;
|
||||
padding: .2rem .4rem;
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
background-color: hsla(0, 0%, 0%, .9);
|
||||
border-radius: .2rem;
|
||||
bottom: calc(var(--poster-aspect) * 5%);
|
||||
color: var(--fg-1);
|
||||
font-size: .8rem;
|
||||
padding: .2rem .4rem;
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
}
|
||||
#toggle_speed { font-weight: 600; }
|
||||
.video {
|
||||
color: var(--fg-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--fg-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.video_title {
|
||||
color: var(--fg-1);
|
||||
margin: 0;
|
||||
color: var(--fg-1);
|
||||
margin: 0;
|
||||
}
|
||||
.videos {
|
||||
column-gap: 1rem;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
row-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
.videos_grid {
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: 16rem auto;
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: 16rem auto;
|
||||
}
|
||||
|
||||
@media (min-width: 80rem) {
|
||||
.playlist_context {
|
||||
left: calc(100% + 2rem);
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
width: 24rem;
|
||||
}
|
||||
.video_info {
|
||||
right: calc(100% + 2rem);
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
width: 24rem;
|
||||
}
|
||||
.playlist_context {
|
||||
left: calc(100% + 2rem);
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
width: 24rem;
|
||||
}
|
||||
.video_info {
|
||||
right: calc(100% + 2rem);
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
width: 24rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 100rem) {
|
||||
.center {
|
||||
max-width: 45vw;
|
||||
}
|
||||
.center {
|
||||
max-width: 45vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@ pub struct Translations {
|
||||
pub failed: String,
|
||||
pub hyper8_logo: String,
|
||||
pub light_color_scheme: String,
|
||||
pub loud_volume: String,
|
||||
pub maximize: String,
|
||||
pub medium_volume: String,
|
||||
pub minimize: String,
|
||||
pub pause: String,
|
||||
pub play: String,
|
||||
pub sound_muted: String,
|
||||
pub video_not_yet_available: String,
|
||||
pub videos: String
|
||||
}
|
||||
@@ -19,6 +26,13 @@ impl Translations {
|
||||
failed: String::from("Fehlgeschlagen"),
|
||||
hyper8_logo: String::from("Hyper 8 Logo"),
|
||||
light_color_scheme: String::from("Helles Farbschema"),
|
||||
loud_volume: String::from("Hohe Lautstärke"),
|
||||
maximize: String::from("Maximieren"),
|
||||
medium_volume: String::from("Mittlere Lautstärke"),
|
||||
minimize: String::from("Minimieren"),
|
||||
pause: String::from("Pausieren"),
|
||||
play: String::from("Abspielen"),
|
||||
sound_muted: String::from("Ton stummgeschalten"),
|
||||
video_not_yet_available: String::from("Video noch nicht verfügbar."),
|
||||
videos: String::from("Videos")
|
||||
}
|
||||
@@ -32,6 +46,13 @@ impl Translations {
|
||||
failed: String::from("Failed"),
|
||||
hyper8_logo: String::from("Hyper 8 Logo"),
|
||||
light_color_scheme: String::from("Light Color Scheme"),
|
||||
loud_volume: String::from("Loud Volume"),
|
||||
maximize: String::from("Maximize"),
|
||||
medium_volume: String::from("Medium Volume"),
|
||||
minimize: String::from("Minimize"),
|
||||
pause: String::from("Pause"),
|
||||
play: String::from("Play"),
|
||||
sound_muted: String::from("Sound muted"),
|
||||
video_not_yet_available: String::from("Video not yet available."),
|
||||
videos: String::from("Videos")
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ pub fn video_html(
|
||||
let index_suffix = context.index_suffix();
|
||||
let description = if let Some(description) = &video.description { description } else { "" };
|
||||
let title = video.title.as_deref().unwrap_or(video.slug());
|
||||
let player_rendered = player(site, video, "");
|
||||
let player_rendered = player(site, video);
|
||||
|
||||
let downloads_widget = downloads(video);
|
||||
|
||||
|
||||
+178
-86
@@ -4,7 +4,7 @@ use indoc::formatdoc;
|
||||
|
||||
use crate::{Site, Video, VideoFile};
|
||||
use crate::icons;
|
||||
use crate::util::{casual_duration, human_size};
|
||||
use crate::util::{casual_duration, human_size, precise_duration};
|
||||
|
||||
pub struct VideoRepresentation {
|
||||
pub saving_factor: f32,
|
||||
@@ -92,7 +92,7 @@ pub fn downloads(video: &Video) -> String {
|
||||
.join("<br>")
|
||||
}
|
||||
|
||||
pub fn player(site: &Site, video: &Video, video_prefix: &str) -> String {
|
||||
pub fn player(site: &Site, video: &Video) -> String {
|
||||
if video.versions.is_empty() {
|
||||
// TODO: Should use poster assets (different sizes)
|
||||
let poster = match &video.poster {
|
||||
@@ -103,7 +103,7 @@ pub fn player(site: &Site, video: &Video, video_prefix: &str) -> String {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!(r#"<img {alt}class="thumbnail" src="{video_prefix}poster.jpg">"#)
|
||||
format!(r#"<img {alt}class="thumbnail" src="poster.jpg">"#)
|
||||
}
|
||||
None => String::new()
|
||||
};
|
||||
@@ -118,70 +118,7 @@ pub fn player(site: &Site, video: &Video, video_prefix: &str) -> String {
|
||||
"#);
|
||||
}
|
||||
|
||||
let representations = compute_representations(&video.versions)
|
||||
.iter()
|
||||
.map(|representation| {
|
||||
let filename = &representation.version.file_name;
|
||||
let format_label = "TODO";
|
||||
let size = human_size(representation.version.file_meta.size);
|
||||
let saving_factor = representation.saving_factor;
|
||||
let saving_percent = (saving_factor * 100.0) as u8;
|
||||
|
||||
formatdoc!(r#"
|
||||
<div class="chart" onclick="document.querySelector('video').setAttribute('src', '{video_prefix}{filename}'); document.querySelector('video').play();">
|
||||
<span class="size">{size}</span>
|
||||
<span class="label">{format_label}</span>
|
||||
<div class="emissions" style="width: calc((1 - {saving_factor}) * 33rem);">
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
<div class="savings" style="width: calc({saving_factor} * 33rem);">
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
<span class="savings_percent" title="-{saving_percent}% less emissions">-{saving_percent}%</span>
|
||||
</div>
|
||||
"#)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let height = video.versions.first().unwrap().video_meta.as_ref().unwrap().height as f32;
|
||||
let width = video.versions.first().unwrap().video_meta.as_ref().unwrap().width as f32;
|
||||
let scale = if width > height { 16.0 / width } else { 9.0 / height };
|
||||
|
||||
let experiment = compute_representations(&video.versions)
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|representation| {
|
||||
let filename = &representation.version.file_name;
|
||||
// let format_label = representation.version.format.label();
|
||||
// let size = human_size(representation.version.size);
|
||||
let saving_factor = representation.saving_factor;
|
||||
// let saving_percent = (saving_factor * 100.0) as u8;
|
||||
let divider = 1.0 - saving_factor; // e.g. 1/4
|
||||
let sqrt = divider.sqrt() * 100.0;
|
||||
// let relative_size_factor = (1.0 - saving_factor.exp2()) * 100.0;
|
||||
|
||||
// 100MB = 1 16x16
|
||||
// 25MB = 1/4 = 0.25 8x8 => *2
|
||||
// 6.25MB = 1/16 = 0.0625 4z4 => *4
|
||||
|
||||
formatdoc!(r#"
|
||||
<div onclick="document.querySelector('video').setAttribute('src', '{video_prefix}{filename}'); document.querySelector('video').play();"
|
||||
style="
|
||||
border: 1px solid black;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: {sqrt}%;
|
||||
position: absolute;
|
||||
width: {sqrt}%;
|
||||
">
|
||||
</div>
|
||||
"#)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
let smallest_version = video.versions.first().unwrap();
|
||||
let smallest_version = video.lowest_resolution_version().unwrap();
|
||||
|
||||
let poster = match &video.poster {
|
||||
Some(poster) => match &poster.assets {
|
||||
@@ -189,23 +126,13 @@ pub fn player(site: &Site, video: &Video, video_prefix: &str) -> String {
|
||||
let largest = poster_assets.largest();
|
||||
let height = largest.height;
|
||||
let width = largest.width;
|
||||
format!(r#"poster="{video_prefix}poster_{width}x{height}.jpg""#)
|
||||
format!(r#"poster="poster_{width}x{height}.jpg""#)
|
||||
}
|
||||
None => format!(r#"poster="{video_prefix}poster.jpg""#)
|
||||
None => format!(r#"poster="poster.jpg""#)
|
||||
}
|
||||
None => String::new()
|
||||
};
|
||||
|
||||
let src = format!("{}{}", video_prefix, smallest_version.file_name);
|
||||
|
||||
let experiment_combined = formatdoc!(
|
||||
r#"
|
||||
<div style="height: {height}rem; position: relative; width: {width}rem;">{experiment}</div>
|
||||
"#,
|
||||
width = width * scale,
|
||||
height = height * scale
|
||||
);
|
||||
|
||||
let subtitles = video.subtitles
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -229,14 +156,72 @@ pub fn player(site: &Site, video: &Video, video_prefix: &str) -> String {
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
let duration = smallest_version.video_meta.as_ref().unwrap().duration;
|
||||
let duration_formatted = precise_duration(duration);
|
||||
let default_resolution_label = smallest_version.vertical_resolution_label();
|
||||
let src = &smallest_version.file_name;
|
||||
|
||||
let versions_ring_rendered = versions_ring(video);
|
||||
|
||||
let maximize_icon = icons::maximize(&site.language.translations.maximize);
|
||||
let minimize_icon = icons::minimize(&site.language.translations.minimize);
|
||||
let pause_icon = icons::pause(&site.language.translations.pause);
|
||||
let play_icon = icons::play(&site.language.translations.play);
|
||||
let volume_loud_icon = icons::volume_loud(&site.language.translations.loud_volume);
|
||||
let volume_medium_icon = icons::volume_medium(&site.language.translations.medium_volume);
|
||||
let volume_muted_icon = icons::volume_muted(&site.language.translations.sound_muted);
|
||||
formatdoc!(r#"
|
||||
<video controls {poster} preload="none" src="{src}">
|
||||
{subtitles}
|
||||
</video>
|
||||
<!--
|
||||
{representations}
|
||||
{experiment_combined}
|
||||
-->
|
||||
<div class="player_wrapper">
|
||||
<div class="player_intro">
|
||||
<button id="default_play">{play_icon}</button>
|
||||
{versions_ring_rendered}
|
||||
</div>
|
||||
<div class="player_controls">
|
||||
<div class="timeline">
|
||||
<div class="bar backdrop"></div>
|
||||
<div class="bar progress"></div>
|
||||
<input aria-valuenow="0" aria-valuetext="00:00" max="{duration}" min="0" step="any" type="range" value="0">
|
||||
</div>
|
||||
<div class="control_pane">
|
||||
<div>
|
||||
<button id="toggle_playback">{play_icon}</button>
|
||||
<button id="toggle_volume">{volume_loud_icon}</button>
|
||||
<button id="toggle_speed">1×</button>
|
||||
<div>
|
||||
<span id="time_current">00:00</span> / <span id="time_total">{duration_formatted}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="select_resolution">{default_resolution_label}</button>
|
||||
<button id="toggle_fullscreen">{maximize_icon}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<video {poster} preload="none" src="{src}">
|
||||
{subtitles}
|
||||
</video>
|
||||
<template id="maximize_icon">
|
||||
{maximize_icon}
|
||||
</template>
|
||||
<template id="minimize_icon">
|
||||
{minimize_icon}
|
||||
</template>
|
||||
<template id="pause_icon">
|
||||
{pause_icon}
|
||||
</template>
|
||||
<template id="play_icon">
|
||||
{play_icon}
|
||||
</template>
|
||||
<template id="volume_loud_icon">
|
||||
{volume_loud_icon}
|
||||
</template>
|
||||
<template id="volume_medium_icon">
|
||||
{volume_medium_icon}
|
||||
</template>
|
||||
<template id="volume_muted_icon">
|
||||
{volume_muted_icon}
|
||||
</template>
|
||||
</div>
|
||||
"#)
|
||||
}
|
||||
|
||||
@@ -284,3 +269,110 @@ pub fn thumbnail(video: &Video, video_prefix: &str, with_duration: bool) -> Stri
|
||||
None => String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn versions_ring(video: &Video) -> String {
|
||||
let radius = 190.0;
|
||||
let diameter = radius * 2.0;
|
||||
|
||||
let mut sizes = Vec::new();
|
||||
let mut total_size: u64 = 0;
|
||||
|
||||
let mut largest_size = 0;
|
||||
|
||||
for version in &video.versions {
|
||||
let src = &version.file_name;
|
||||
let size = version.file_meta.size;
|
||||
let resolution = version.vertical_resolution_label();
|
||||
sizes.push((size, src, resolution));
|
||||
total_size += size;
|
||||
|
||||
if size > largest_size {
|
||||
largest_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
sizes.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Draws a ring segment. In clock terms we start at 12 o'clock and we go clockwise.
|
||||
let slice = |arc_angle: f32, begin_angle: f32, lightness: f32, src: &str, tab_index: usize, resolution: &str, size: &str| -> String {
|
||||
let fill = format!("hsl(0, 0%, {lightness}%)");
|
||||
let large_arc_flag = if arc_angle < 180.0 { 0 } else { 1 };
|
||||
|
||||
let begin_angle_rad = begin_angle.to_radians();
|
||||
let begin_x = begin_angle_rad.sin();
|
||||
let begin_y = -begin_angle_rad.cos();
|
||||
|
||||
let end_angle_rad = (begin_angle + arc_angle).to_radians();
|
||||
let end_x = end_angle_rad.sin();
|
||||
let end_y = -end_angle_rad.cos();
|
||||
|
||||
let outer_radius = radius * 0.6;
|
||||
let segment_outer_begin_x = radius + begin_x * outer_radius;
|
||||
let segment_outer_begin_y = radius + begin_y * outer_radius;
|
||||
|
||||
let segment_outer_end_x = radius + end_x * outer_radius;
|
||||
let segment_outer_end_y = radius + end_y * outer_radius;
|
||||
|
||||
let inner_radius = radius * 0.5;
|
||||
let segment_inner_begin_x = radius + begin_x * inner_radius;
|
||||
let segment_inner_begin_y = radius + begin_y * inner_radius;
|
||||
|
||||
let segment_inner_end_x = radius + end_x * inner_radius;
|
||||
let segment_inner_end_y = radius + end_y * inner_radius;
|
||||
|
||||
let text_radius = radius * 0.8;
|
||||
let text_angle_rad = (begin_angle + 0.5 * arc_angle).to_radians();
|
||||
let text_x = radius + text_angle_rad.sin() * text_radius;
|
||||
let text_resolution_y = radius + -text_angle_rad.cos() * text_radius - 9.0;
|
||||
let text_size_y = radius + -text_angle_rad.cos() * text_radius + 9.0;
|
||||
|
||||
formatdoc!(r#"
|
||||
<path
|
||||
d="M {segment_outer_begin_x},{segment_outer_begin_y}
|
||||
A {outer_radius} {outer_radius} 0 {large_arc_flag} 1 {segment_outer_end_x},{segment_outer_end_y}
|
||||
L {segment_inner_end_x},{segment_inner_end_y}
|
||||
A {inner_radius} {inner_radius} 0 {large_arc_flag} 0 {segment_inner_begin_x},{segment_inner_begin_y}
|
||||
Z"
|
||||
data-label="{resolution}"
|
||||
data-src="{src}"
|
||||
fill="{fill}"
|
||||
tabindex="{tab_index}">
|
||||
<title>{resolution} {size}</title>
|
||||
</path>
|
||||
<text text-anchor="middle" x="{text_x}" y="{text_resolution_y}">
|
||||
{resolution}
|
||||
</text>
|
||||
<text class="size" text-anchor="middle" x="{text_x}" y="{text_size_y}">
|
||||
{size}
|
||||
</text>
|
||||
"#)
|
||||
};
|
||||
|
||||
let mut slices = String::new();
|
||||
let gap_angle = 2.5;
|
||||
let usable_angle = 360.0 - sizes.len() as f32 * gap_angle;
|
||||
let mut begin_angle = gap_angle * 0.5;
|
||||
|
||||
for (index, (size, src, title)) in sizes.iter().enumerate() {
|
||||
let arc_angle = if sizes.len() == 1 {
|
||||
usable_angle
|
||||
} else {
|
||||
(*size as f32 / total_size as f32) * usable_angle
|
||||
};
|
||||
|
||||
let offset_fill = index as f32 / (sizes.len() - 1) as f32;
|
||||
let lightness = 75.0 - offset_fill * 50.0;
|
||||
|
||||
let size_humanized = human_size(*size);
|
||||
|
||||
slices.push_str(&slice(arc_angle, begin_angle, lightness, src, index + 1, title, &size_humanized));
|
||||
|
||||
begin_angle += arc_angle + gap_angle;
|
||||
}
|
||||
|
||||
formatdoc!(r#"
|
||||
<svg class="versions_ring" width="{diameter}" height="{diameter}" version="1.1" viewBox="0 0 {diameter} {diameter}" xmlns="http://www.w3.org/2000/svg">
|
||||
{slices}
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,44 @@ pub fn logo(label: &str) -> String {
|
||||
"#)
|
||||
}
|
||||
|
||||
|
||||
pub fn maximize(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m4.2695 59.731h17.506v-7.0907h-10.415v-10.415h-7.0907zm55.462 0v-17.506h-7.0907v10.415h-10.415v7.0907zm0-55.462h-17.506v7.0907h10.415v10.415h7.0907zm-55.462 0v17.506h7.0907v-10.415h10.415v-7.0907z"/>
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
pub fn minimize(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m36.27 27.731h17.506v-7.0907h-10.415v-10.415h-7.0907zm-8.5385 0v-17.506h-7.0907v10.415h-10.415v7.0907zm0 8.5385h-17.506v7.0907h10.415v10.415h7.0907zm8.5385 0v17.506h7.0907v-10.415h10.415v-7.0907z"/>
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
pub fn pause(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m36.915 56.995v-49.985h15.592v49.985zm-25.421 0v-49.985h15.592v49.985z">
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
|
||||
pub fn play(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m11.116 59.677 45.476-26.453-45.544-28.093z"/>
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
pub fn playlist(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -71,3 +109,30 @@ pub fn video(label: &str) -> String {
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
pub fn volume_loud(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m33.82 11.047-13.967 12.195h-6.0586c-2.4434 0-4.4121 1.9668-4.4121 4.4102v9.1797c0 2.4434 1.9688 4.4102 4.4121 4.4102h6.0586l13.967 12.193v-42.389zm4.2227 1.7285a2.4605 2.4605 0 0 0-2.4609 2.459 2.4605 2.4605 0 0 0 2.4609 2.4609c8.1842 0 14.76 6.5605 14.76 14.709 1e-6 8.1485-6.5756 14.709-14.76 14.709a2.4605 2.4605 0 0 0-2.4609 2.459 2.4605 2.4605 0 0 0 2.4609 2.4609c10.836 0 19.68-8.8131 19.68-19.629 0-10.816-8.8438-19.629-19.68-19.629zm0 9.2793a2.4605 2.4605 0 0 0-2.4609 2.4609 2.4605 2.4605 0 0 0 2.4609 2.4609c3.0438 0 5.4512 2.4046 5.4512 5.4277 0 3.0232-2.4074 5.4277-5.4512 5.4277a2.4605 2.4605 0 0 0-2.4609 2.4609 2.4605 2.4605 0 0 0 2.4609 2.4609c5.6955 0 10.373-4.6591 10.373-10.35s-4.6775-10.35-10.373-10.35z"/>
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
pub fn volume_medium(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m33.82 11.047-13.967 12.195h-6.0586c-2.4434 0-4.4121 1.9668-4.4121 4.4102v9.1797c0 2.4434 1.9688 4.4102 4.4121 4.4102h6.0586l13.967 12.193zm4.2227 11.008c-1.3592-2.42e-4 -2.4612 1.1017-2.4609 2.4609-2.41e-4 1.3592 1.1017 2.4612 2.4609 2.4609 3.0438 0 5.4512 2.4046 5.4512 5.4277 0 3.0232-2.4074 5.4277-5.4512 5.4277-1.3592-2.42e-4 -2.4612 1.1017-2.4609 2.4609-2.41e-4 1.3592 1.1017 2.4612 2.4609 2.4609 5.6955 0 10.373-4.6591 10.373-10.35s-4.6775-10.35-10.373-10.35z"/>
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
|
||||
pub fn volume_muted(label: &str) -> String {
|
||||
formatdoc!(r#"
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{label}</title>
|
||||
<path d="m33.82 11.047-13.967 12.195h-6.0586c-2.4434 0-4.4121 1.9668-4.4121 4.4102v9.1797c0 2.4434 1.9688 4.4102 4.4121 4.4102h6.0586l13.967 12.193v-42.389zm4.1875 11.416a2.4605 2.4605 0 0 0-1.7402 0.7207 2.4605 2.4605 0 0 0 0 3.4805l5.9844 5.9844-5.9844 5.9844a2.4605 2.4605 0 0 0 0 3.4785 2.4605 2.4605 0 0 0 3.4805 0l5.9824-5.9824 5.9844 5.9824a2.4605 2.4605 0 0 0 3.4805 0 2.4605 2.4605 0 0 0 0-3.4785l-5.9844-5.9844 5.9844-5.9844a2.4605 2.4605 0 0 0 0-3.4805 2.4605 2.4605 0 0 0-1.7402-0.7207 2.4605 2.4605 0 0 0-1.7402 0.7207l-5.9844 5.9844-5.9824-5.9844a2.4605 2.4605 0 0 0-1.7402-0.7207z"/>
|
||||
</svg>
|
||||
"#)
|
||||
}
|
||||
@@ -24,6 +24,8 @@ pub struct ThemeVars {
|
||||
/// Background 1 (furthest away/bottom layer)
|
||||
pub bg_1: String,
|
||||
/// Background 2 (further away)
|
||||
pub bg_1_overlay: String,
|
||||
/// Background 2 (further away) with some transparency
|
||||
pub bg_2: String,
|
||||
/// Background 3 (far away)
|
||||
pub bg_3: String,
|
||||
@@ -85,6 +87,7 @@ impl ThemeVars {
|
||||
ThemeVars {
|
||||
accent: String::from("#fbdf46"),
|
||||
bg_1: String::from("#161616"),
|
||||
bg_1_overlay: String::from("rgba(22, 22, 22, .92)"),
|
||||
bg_2: String::from("#262626"),
|
||||
bg_3: String::from("#333"),
|
||||
bg_mg: String::from("#444"),
|
||||
@@ -98,6 +101,7 @@ impl ThemeVars {
|
||||
pub fn light() -> ThemeVars {
|
||||
ThemeVars {
|
||||
accent: String::from("#9f2e02"),
|
||||
bg_1_overlay: String::from("rgba(255, 255, 255, .92)"),
|
||||
bg_1: String::from("#fff"),
|
||||
bg_2: String::from("#eee"),
|
||||
bg_3: String::from("#ddd"),
|
||||
@@ -112,6 +116,7 @@ impl ThemeVars {
|
||||
pub fn print(&self) -> String {
|
||||
let accent = &self.accent;
|
||||
let bg_1 = &self.bg_1;
|
||||
let bg_1_overlay = &self.bg_1_overlay;
|
||||
let bg_2 = &self.bg_2;
|
||||
let bg_3 = &self.bg_3;
|
||||
let bg_mg = &self.bg_mg;
|
||||
@@ -123,6 +128,7 @@ impl ThemeVars {
|
||||
formatdoc!(r#"
|
||||
--accent: {accent};
|
||||
--bg-1: {bg_1};
|
||||
--bg-1-overlay: {bg_1_overlay};
|
||||
--bg-2: {bg_2};
|
||||
--bg-3: {bg_3};
|
||||
--bg-mg: {bg_mg};
|
||||
|
||||
+46
-1
@@ -19,7 +19,7 @@ use crate::{
|
||||
SubtitleFile,
|
||||
VideoMetaCached
|
||||
};
|
||||
use crate::util::hash;
|
||||
use crate::util::{hash, human_size};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Video {
|
||||
@@ -233,6 +233,21 @@ impl Video {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn lowest_resolution_version(&self) -> Option<&VideoFile> {
|
||||
let mut version_candidate: Option<(usize, &VideoFile)> = None;
|
||||
|
||||
for version in &self.versions {
|
||||
if let Some(video_meta) = &version.video_meta {
|
||||
let pixels = video_meta.width * video_meta.height;
|
||||
if version_candidate.is_none() || version_candidate.is_some_and(|pixels_version| pixels < pixels_version.0) {
|
||||
version_candidate = Some((pixels, version));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
version_candidate.map(|pixels_version| pixels_version.1)
|
||||
}
|
||||
|
||||
fn new(path: String) -> Video {
|
||||
Video {
|
||||
description: None,
|
||||
@@ -396,6 +411,36 @@ impl VideoFile {
|
||||
video_meta
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolution_label(&self) -> String {
|
||||
match &self.video_meta {
|
||||
Some(video_meta) => {
|
||||
let height = video_meta.height;
|
||||
let width = video_meta.width;
|
||||
format!("{width}×{height}")
|
||||
}
|
||||
None => String::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size_label(&self) -> String {
|
||||
let size = human_size(self.file_meta.size);
|
||||
match &self.video_meta {
|
||||
Some(video_meta) => {
|
||||
let height = video_meta.height;
|
||||
let width = video_meta.width;
|
||||
format!("{width}×{height} ({size})")
|
||||
}
|
||||
None => size
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical_resolution_label(&self) -> String {
|
||||
match &self.video_meta {
|
||||
Some(video_meta) => format!("{}p", video_meta.height),
|
||||
None => String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoMeta {
|
||||
|
||||
Reference in New Issue
Block a user