Support toggling the playlist sidebar for a session, refactor script globals

This commit is contained in:
Simon Repp
2025-08-11 12:07:08 +02:00
parent b17c8b712d
commit bfe13c9a28
9 changed files with 105 additions and 54 deletions
+7 -9
View File
@@ -16,8 +16,6 @@ const siteTreeElements = siteTree.querySelector('.elements');
const siteTreeStatus = siteTree.querySelector('[role="status"]');
const indexSuffix = window.location.pathname.endsWith('index.html') ? 'index.html' : '';
const rootPrefix = document.querySelector('.home').dataset.rootPrefix;
const sitePath = document.querySelector('.home').dataset.sitePath;
// We only render the site tree elements (which are displayed during
// browsing/searching) into the DOM when, or shortly before, we need them,
@@ -75,7 +73,7 @@ function initializeSiteTree() {
let image;
if (video.image) {
image = document.createElement('img');
image.src = rootPrefix + video.sitePath + video.image;
image.src = window.hyper8.rootPrefix + video.sitePath + video.image;
if (video.lqip) {
image.style.background = lqipToRadialGradient(video.lqip);
@@ -105,11 +103,11 @@ function initializeSiteTree() {
video.element = document.createElement('a');
video.element.classList.add('video');
video.element.href = rootPrefix + video.sitePath + indexSuffix;
video.element.href = window.hyper8.rootPrefix + video.sitePath + indexSuffix;
video.element.appendChild(image);
video.element.appendChild(divTitleDetails);
if (video.sitePath === sitePath) {
if (video.sitePath === window.hyper8.sitePath) {
video.element.classList.add('current');
}
@@ -145,7 +143,7 @@ function initializeSiteTree() {
playlist.element = document.createElement('a');
playlist.element.classList.add('playlist');
playlist.element.href = rootPrefix + playlist.sitePath + indexSuffix;
playlist.element.href = window.hyper8.rootPrefix + playlist.sitePath + indexSuffix;
playlist.element.appendChild(playlist.expandToggle);
playlist.element.appendChild(spanTitle);
playlist.element.appendChild(spanInfo);
@@ -160,7 +158,7 @@ function initializeSiteTree() {
});
}
if (playlist.sitePath === sitePath) {
if (playlist.sitePath === window.hyper8.sitePath) {
playlist.element.classList.add('current');
}
@@ -197,7 +195,7 @@ function initializeSiteTree() {
collection.element = document.createElement('a');
collection.element.classList.add('collection');
collection.element.href = rootPrefix + collection.sitePath + indexSuffix;
collection.element.href = window.hyper8.rootPrefix + collection.sitePath + indexSuffix;
collection.element.appendChild(collection.expandToggle);
collection.element.appendChild(spanTitle);
@@ -216,7 +214,7 @@ function initializeSiteTree() {
});
}
if (collection.sitePath === sitePath) {
if (collection.sitePath === window.hyper8.sitePath) {
collection.element.classList.add('current');
}
+4 -7
View File
@@ -289,10 +289,6 @@ img {
padding: .5rem;
}
.medium_font { font-size: 1.2rem; }
.outline {
flex-shrink: 0;
min-width: 20rem;
}
.outro_playlist_context > a,
.outro_playlist_context > button {
align-items: center;
@@ -634,9 +630,10 @@ img {
.search input:focus::placeholder { opacity: 0; }
.search:not(.query_present) button { display: none; }
.sidebar {
display: flex;
flex-direction: column;
flex-shrink: 0;
min-width: 20rem;
}
.sidebar.hidden { display: none; }
.site_title {
color: var(--fg-2);
font-size: 1.17rem;
@@ -974,7 +971,7 @@ video {
.feed_grid { gap: 2rem; }
}
@media (min-width: 80rem) {
.outline {
.sidebar {
max-height: calc(100dvh - 4rem);
overflow-y: auto;
width: 24rem;
+18
View File
@@ -20,6 +20,7 @@ const speedControl = document.querySelector('#speed');
const speedMultiplier = document.querySelector('#speed .multiplier');
const timelineInput = document.querySelector('.timeline input');
const togglePlaybackButton = document.querySelector('#toggle_playback');
const togglePlaylistButton = document.querySelector('#toggle_playlist');
const versionSelectorControl = document.querySelector('.selector_control.version');
const versionSlices = document.querySelectorAll('.versions_ring g[data-src]');
const versionsRing = document.querySelector('.versions_ring');
@@ -436,6 +437,23 @@ speedControl.addEventListener('wheel', event => {
togglePlaybackButton.addEventListener('click', togglePlayback);
if (togglePlaylistButton) {
const sidebar = document.querySelector('.sidebar');
const storageKey = `hyper8HidePlaylist:${togglePlaylistButton.dataset.sitePath}`;
function togglePlaylist() {
if (sidebar.classList.contains('hidden')) {
sessionStorage.removeItem(storageKey);
sidebar.classList.remove('hidden');
} else {
sessionStorage.setItem(storageKey, '')
sidebar.classList.add('hidden');
}
}
togglePlaylistButton.addEventListener('click', togglePlaylist);
}
video.addEventListener('ended', () => {
playerWrapper.dataset.state = 'outro';
clearInterval(playbackUpdateInterval);
+4 -1
View File
@@ -208,10 +208,13 @@ impl Layout {
let t_video_player = translations.video_player;
let body = formatdoc!(r##"
<body>
<script>
window.hyper8 = {{ rootPrefix: '{root_prefix}', sitePath: '{site_path}' }};
</script>
<div class="layout">
<header>
<div class="navigation">
<a class="home" data-root-prefix="{root_prefix}" data-site-path="{site_path}" href="{root_url}">
<a class="home" href="{root_url}">
{logo_polychrome_icon}
<span class="title">{e_site_title}</span>
</a>
+60 -31
View File
@@ -50,20 +50,6 @@ pub fn video_html(
let title = video.title_or_slug_or_generic_label(translations);
let e_title = html_escape_outside_attribute(title);
let r_downloads = if video.download() && !video.versions.is_empty() {
let t_download = translations.download;
let t_download_permalink = translations.download_permalink;
let download_icon = icons::DOWNLOAD;
formatdoc!(r#"
<a href="{t_download_permalink}{index_suffix}">
{download_icon} {t_download}
</a>
"#)
} else {
String::new()
};
let parent_context = if traversal.steps.len() > 1 {
let parent_step = &traversal.steps[traversal.steps.len() - 2];
parent_step.context_with_bubbles("../", site)
@@ -80,7 +66,9 @@ pub fn video_html(
video_prefix
);
let copy_link_button = if video.copy_link() {
let mut actions = Vec::new();
if video.copy_link() {
let (content_key, content_value) = match &site.base_url {
Some(base_url) => ("content", base_url.join_index(site, &video.site_path)),
None => ("dynamic-url", String::new())
@@ -88,25 +76,52 @@ pub fn video_html(
layout.add_clipboard_script();
copy_button(content_key, &content_value, translations.copy_link)
} else {
String::new()
};
let copy_link = copy_button(content_key, &content_value, translations.copy_link);
let r_embed = if video.embedding() && site.base_url.is_some() && !video.versions.is_empty() {
actions.push(copy_link);
}
if video.embedding() && site.base_url.is_some() && !video.versions.is_empty() {
let embed_icon = icons::EMBED;
let t_embed = &translations.embed;
let t_embed_codes_permalink = &translations.embed_codes_permalink;
format!(r#"
let embed_action = format!(r#"
<a href="{t_embed_codes_permalink}{index_suffix}">
{embed_icon} {t_embed}
</a>
"#)
} else {
String::new()
};
"#);
actions.push(embed_action);
}
if video.download() && !video.versions.is_empty() {
let t_download = translations.download;
let t_download_permalink = translations.download_permalink;
let download_icon = icons::DOWNLOAD;
let download = formatdoc!(r#"
<a href="{t_download_permalink}{index_suffix}">
{download_icon} {t_download}
</a>
"#);
actions.push(download);
}
if let Some(playlist) = playlist {
let t_playlist = translations.playlist;
let playlist_icon = icons::playlist(None);
let playlist_site_path = playlist.site_path.normalized();
let toggle_playlist = formatdoc!(r#"
<button data-site-path="{playlist_site_path}" id="toggle_playlist">
{playlist_icon} {t_playlist}
</button>
"#);
actions.push(toggle_playlist);
}
let release_date = match video.release_date {
Some(release_date) => {
@@ -125,6 +140,17 @@ pub fn video_html(
let links_rendered = links(&site.language, &video.links);
let actions = if actions.is_empty() {
String::new()
} else {
let actions_joined = actions.join("\n");
formatdoc!(r#"
<div class="actions align flex space">
{actions_joined}
</div>
"#)
};
let video_content = formatdoc!(r#"
<div>
<div class="video_info top">
@@ -137,11 +163,7 @@ pub fn video_html(
<div>
{release_date}
</div>
<div class="actions align flex space">
{copy_link_button}
{r_embed}
{r_downloads}
</div>
{actions}
</div>
{links_rendered}
{description}
@@ -159,12 +181,19 @@ pub fn video_html(
Some(video)
);
let playlist_site_path = playlist.site_path.normalized();
formatdoc!(r#"
<div class="layout_split">
{video_content}
<div class="outline">
<div class="sidebar">
{playlist_context}
</div>
<script>
if (sessionStorage.getItem('hyper8HidePlaylist:{playlist_site_path}') !== null) {{
const sidebar = document.currentScript.previousElementSibling;
sidebar.classList.add('hidden');
}}
</script>
</div>
"#)
}
+1 -1
View File
@@ -88,7 +88,7 @@ pub async fn index(context: Data<Arc<Context>>) -> HttpResponse {
fn empty_site_html(translations: &Translations) -> String {
let collection_icon = icons::collection(translations.collection);
let playlist_icon = icons::playlist(translations.playlist);
let playlist_icon = icons::playlist(None);
let video_icon = icons::video(translations.video);
let t_a_site_presenting_a_single_playlist_of_videos = translations.a_site_presenting_a_single_playlist_of_videos;
+1 -1
View File
@@ -305,7 +305,7 @@ pub fn edit_page(
translations
);
let playlist_icon = icons::playlist(translations.playlist);
let playlist_icon = icons::playlist(Some(translations.playlist));
let playlist_title = playlist.title_or_slug_or_generic_label(translations);
let t_new_video = translations.new_video;
+1 -1
View File
@@ -348,7 +348,7 @@ fn playlist_node(
breadcrumbs.push(format!(r#"<a href="{exposed_edit_url}">{e_breadcrumb_title}</a>"#));
}
let playlist_icon = icons::playlist(translations.playlist);
let playlist_icon = icons::playlist(Some(translations.playlist));
let playlist_visibility = Visibility::from_playlist(playlist);
let node = tree_node(
+9 -3
View File
@@ -303,10 +303,16 @@ pub fn play(label: &str) -> String {
"#)
}
pub fn playlist(label: &str) -> String {
/// Video viewport rectangles that are subtly stacked towards the distance, growing fainter with distance
pub fn playlist(label: Option<&str>) -> String {
let (aria_hidden, title) = match label {
Some(label) => ("", format!("<title>{label}</title>")),
None => (r#"aria-hidden="true""#, String::new())
};
formatdoc!(r#"
<svg width="1em" height="1em" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<title>{label}</title>
<svg {aria_hidden} width="1em" height="1em" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
{title}
<path d="m15.502 29.753c-1.8558 2e-6 -3.3624 1.5043-3.3624 3.3601v18.998c0 0.92794 0.37558 1.7683 0.98368 2.3764 0.15202 0.15202 0.31779 0.29066 0.49657 0.41144 0.53661 0.36295 1.1856 0.5746 1.8822 0.5746h32.996c0.92794 0 1.7683-0.37794 2.3764-0.98604 0.15253-0.15238 0.28796-0.31729 0.40908-0.49657 0.36233-0.53632 0.57696-1.1839 0.57696-1.8798v-18.998c0-1.8558-1.5066-3.3601-3.3624-3.3601z"></path>
<path d="m15.622 19.14c-1.8558 2e-6 -3.3613 1.5035-3.3613 3.3594v1.9473h39.479v-1.9473c0-1.8559-1.5055-3.3594-3.3613-3.3594h-20.527z" opacity=".666"></path>
<path d="m15.622 8.5264c-1.8558 2e-6 -3.3613 1.5035-3.3613 3.3594v1.9473h39.479v-1.9473c0-1.8559-1.5055-3.3594-3.3613-3.3594h-20.527z" opacity=".333"></path>