diff --git a/src/build/assets/search.js b/src/build/assets/search.js index 734868c..69160d0 100644 --- a/src/build/assets/search.js +++ b/src/build/assets/search.js @@ -18,26 +18,95 @@ let searchInitialized = false; let delayedUpdateInterval = null; function initializeResults() { - for (const item of SITE_ITEMS) { + // Create the search result element for the video inside the DOM, store a + // reference to it in the siteContent tree + function initializeVideo(video) { let image; - if (item.image) { + if (video.image) { image = document.createElement('img'); - image.src = rootPrefix + item.url + item.image; + image.src = rootPrefix + video.sitePath + video.image; } else { image = document.createElement('span'); image.classList.add('placeholder'); } const spanText = document.createElement('span'); - spanText.dataset.searchable = 'true'; - spanText.textContent = item.title; + spanText.textContent = video.title; - const aRow = document.createElement('a'); - aRow.href = rootPrefix + item.url + indexSuffix; - aRow.appendChild(image); - aRow.appendChild(spanText); + video.element = document.createElement('a'); + video.element.classList.add('video'); + video.element.href = rootPrefix + video.sitePath + indexSuffix; + video.element.appendChild(image); + video.element.appendChild(spanText); - searchResults.appendChild(aRow); + searchResults.appendChild(video.element); + } + + // Create the search result element for the playlist inside the DOM, store + // a reference to it in the siteContent tree + function initializePlaylist(playlist, ancestors = []) { + const traversal = [...ancestors, playlist]; + + const spanText = document.createElement('span'); + spanText.textContent = traversal.map(ancestor => ancestor.title).join(' > '); + + playlist.element = document.createElement('a'); + playlist.element.classList.add('playlist'); + playlist.element.href = rootPrefix + playlist.sitePath + indexSuffix; + playlist.element.appendChild(spanText); + + searchResults.appendChild(playlist.element); + + for (const video of playlist.videos) { + initializeVideo(video); + } + + } + + // Create the search result element for the collection inside the DOM, + // store a reference to it in the siteContent tree + function initializeCollection(collection, ancestors = []) { + const traversal = [...ancestors, collection]; + + const spanText = document.createElement('span'); + spanText.textContent = traversal.map(ancestor => ancestor.title).join(' > '); + + collection.element = document.createElement('a'); + collection.element.classList.add('collection'); + collection.element.href = rootPrefix + collection.sitePath + indexSuffix; + collection.element.appendChild(spanText); + + searchResults.appendChild(collection.element); + + for (const video of collection.videos) { + initializeVideo(video); + } + + for (const playlist of collection.playlists) { + initializePlaylist(playlist, traversal); + } + + for (const subcollection of collection.subcollections) { + initializeCollection(subcollection, traversal); + } + } + + if (siteContent.collection) { + for (const video of siteContent.collection.videos) { + initializeVideo(video); + } + + for (const playlist of siteContent.collection.playlists) { + initializePlaylist(playlist); + } + + for (const subcollection of siteContent.collection.subcollections) { + initializeCollection(subcollection); + } + } else /* if (siteContent.playlist) */ { + for (const video of siteContent.playlist.videos) { + initializeVideo(video); + } } searchInitialized = true; @@ -58,17 +127,69 @@ function updateResults() { const regexp = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); let shown = 0; - for (const row of searchResults.children) { - const title = row.querySelector('[data-searchable]').textContent; - const display = regexp.test(title); - row.style.setProperty('display', display ? null : 'none'); + function updateVideo(video) { + const display = regexp.test(video.title); + video.element.classList.toggle('visible', display); if (display) { shown += 1; } + return display; + } + + function updatePlaylist(playlist) { + let display = regexp.test(playlist.title); + if (display) { shown += 1; } + + for (const video of playlist.videos) { + display = updateVideo(video) || display; + } + + playlist.element.classList.toggle('visible', display); + return display; + } + + function updateCollection(collection) { + let display = regexp.test(collection.title); + if (display) { shown += 1; } + + for (const video of collection.videos) { + display = updateVideo(video) || display; + } + + for (const playlist of collection.playlists) { + updatePlaylist(playlist); + } + + for (const subcollection of collection.subcollections) { + updateCollection(subcollection); + } + + collection.element.classList.toggle('visible', display); + return display; + } + + if (siteContent.collection) { + for (const video of siteContent.collection.videos) { + updateVideo(video); + } + + for (const playlist of siteContent.collection.playlists) { + updatePlaylist(playlist); + } + + for (const subcollection of siteContent.collection.subcollections) { + updateCollection(subcollection); + } + } else { + for (const video of siteContent.playlist.videos) { + updateVideo(video); + } } if (shown === 0) { + searchResults.style.setProperty('display', 'none'); statusField.removeAttribute('aria-label'); statusField.textContent = SEARCH_JS_T.nothingFoundForXxx(query); } else { + searchResults.style.setProperty('display', null); statusField.setAttribute('aria-label', SEARCH_JS_T.showingXxxResultsForXxx(shown, query)); statusField.textContent = ''; } @@ -109,7 +230,7 @@ searchInput.addEventListener('input', () => { searchContainer.addEventListener('keydown', event => { if (event.key === 'ArrowUp') { - const visibleResults = [...searchResults.children].filter(row => row.style.display !== 'none'); + const visibleResults = [...searchResults.querySelectorAll('a.visible')]; if (visibleResults.length) { const targetRowIndex = visibleResults.indexOf(event.target); @@ -124,7 +245,7 @@ searchContainer.addEventListener('keydown', event => { } event.preventDefault(); } else if (event.key === 'ArrowDown') { - const visibleResults = [...searchResults.children].filter(row => row.style.display !== 'none'); + const visibleResults = [...searchResults.querySelectorAll('a.visible')]; if (visibleResults.length) { const targetRowIndex = visibleResults.indexOf(event.target); diff --git a/src/build/assets/site.css b/src/build/assets/site.css index 88c780f..958827a 100644 --- a/src/build/assets/site.css +++ b/src/build/assets/site.css @@ -331,27 +331,6 @@ img { @media (max-aspect-ratio: 2/3) { .player_wrapper:not(:fullscreen) video { max-height: 76dvh; } } -.playlist { - display: flex; - flex-direction: column; -} -.playlist > a > div { flex-shrink: 0; } -.playlist > * { - align-items: center; - border-radius: .5rem; - column-gap: 1rem; - display: flex; - outline: none; - padding: .5rem; -} -.playlist > *:focus-visible, -.playlist > *:hover { background: var(--bg-2); } -.playlist .description { - align-items: center; - color: var(--fg-1); - display: flex; - flex-grow: 1; -} .playlist_context { border: 1px solid var(--bg-2); border-radius: .5rem; @@ -431,6 +410,27 @@ img { color: var(--fg-1); display: block; } +.playlist_page { + display: flex; + flex-direction: column; +} +.playlist_page > a > div { flex-shrink: 0; } +.playlist_page > * { + align-items: center; + border-radius: .5rem; + column-gap: 1rem; + display: flex; + outline: none; + padding: .5rem; +} +.playlist_page > *:focus-visible, +.playlist_page > *:hover { background: var(--bg-2); } +.playlist_page .description { + align-items: center; + color: var(--fg-1); + display: flex; + flex-grow: 1; +} .reading_width { max-width: 32rem; } .release_date { color: var(--fg-3); @@ -471,6 +471,7 @@ img { max-height: 80dvh; min-width: 20rem; overflow-y: auto; + padding: 1rem; position: absolute; right: 0; z-index: 1; @@ -480,25 +481,24 @@ img { flex-direction: column; width: 100%; } -.search .results > a { - display: flex; - column-gap: .5rem; -} .search .results a { align-items: center; column-gap: .7rem; display: flex; + padding: .5rem; } -.search .results > a:focus-visible, -.search .results > a:hover { +.search .results a:focus-visible, +.search .results a:hover { background: var(--bg-2); + color: var(--fg-1); outline: none; } -.search .results a > :nth-child(1) { +.search .results a:not(.visible) { display: none; } +.search .results .video > :nth-child(1) { aspect-ratio: var(--poster-aspect); height: 3rem; } -.search .results a > :nth-child(2) { flex: 1; } +.search .results .video > :nth-child(2) { flex: 1; } .search .results .placeholder { background: var(--bg-3); } .search:not(.open) button, .search:not(.open) .pane { @@ -635,11 +635,6 @@ video { height: 100%; width: 100%; } -.video { - color: var(--fg-2); - display: flex; - flex-direction: column; -} .video_title { color: var(--fg-1); margin: 0; @@ -668,11 +663,11 @@ video { .videos_grid .thumbnail { height: auto; } @media (max-width: 33.999rem) { - .playlist > a > div { width: 50%; } - .playlist .thumbnail { height: auto; } + .playlist_page > a > div { width: 50%; } + .playlist_page .thumbnail { height: auto; } } @media (min-width: 34rem) { - .playlist .thumbnail { + .playlist_page .thumbnail { height: 8rem; width: auto; } diff --git a/src/build/assets/video.js b/src/build/assets/video.js index ea35fe9..a40a751 100644 --- a/src/build/assets/video.js +++ b/src/build/assets/video.js @@ -428,7 +428,7 @@ video.addEventListener('ended', () => { video.addEventListener('click', togglePlayback); window.addEventListener('keydown', event => { - if (searchContainer.contains(event.target)) return; + if (searchContainer?.contains(event.target)) return; if (event.key === ' ') { togglePlayback(); diff --git a/src/build/layout.rs b/src/build/layout.rs index 4126d56..61300e2 100644 --- a/src/build/layout.rs +++ b/src/build/layout.rs @@ -100,7 +100,30 @@ impl Layout { String::new() }; - let search_js_asset_hash = asset_hashes.search_js.as_ref().unwrap(); + let search; + let search_script; + if let Some(search_js_asset_hash) = &asset_hashes.search_js { + let search_icon = icons::search(translations.search); + let close_icon = icons::failure(translations.close); + let t_search = translations.search; + search = formatdoc!(r#" + + "#); + search_script = format!(r#""#); + } else { + search = String::new(); + search_script = String::new(); + }; let head = formatdoc!(r#" @@ -113,7 +136,7 @@ impl Layout { {adaptive_theme_script} {clipboard_script} {video_script} - + {search_script} "#); @@ -152,9 +175,6 @@ impl Layout { templates.push_str(&player_icon_templates(translations)); } - let search_icon = icons::search(translations.search); - let close_icon = icons::failure(translations.close); - let t_search = translations.search; let body = formatdoc!(r##"
@@ -178,17 +198,7 @@ impl Layout { Hyper 8 Video System {version_display}
- + {search} {templates} diff --git a/src/build/playlist.rs b/src/build/playlist.rs index 1cb6fd6..e38b57d 100644 --- a/src/build/playlist.rs +++ b/src/build/playlist.rs @@ -112,7 +112,7 @@ pub fn playlist_html( .join("\n"); let videos_rendered = formatdoc!(r#" -
+
{videos}
"#); diff --git a/src/build/scripts.rs b/src/build/scripts.rs index adc726c..3a8793e 100644 --- a/src/build/scripts.rs +++ b/src/build/scripts.rs @@ -5,7 +5,15 @@ use std::fs; use indoc::formatdoc; -use crate::{Context, Site, SiteItem}; +use crate::{ + Collection, + Context, + Playlist, + Site, + SiteContent, + Translations, + Video +}; use crate::util::url_safe_hash_base64; use super::AssetHashes; @@ -38,82 +46,109 @@ pub fn generate_search_js( ) { let translations = &site.language.translations; - let mut items = Vec::new(); - - let mut visit = |item: SiteItem| { - let image; - let kind; - let title; - let site_path; - - match item { - SiteItem::Collection(collection) => { - image = None; - kind = "collection"; - title = collection.title_or_slug_or_generic_label(&translations); - site_path = collection.site_path.normalized(); - } - SiteItem::Playlist(playlist) => { - image = None; - kind = "playlist"; - title = playlist.title_or_slug_or_generic_label(&translations); - site_path = playlist.site_path.normalized(); - } - SiteItem::Video(video) => { - image = video.poster - .as_ref() - .and_then(|poster| poster.assets.as_ref()) - .map(|assets| assets.max_160.target_filename("poster")); - kind = "video"; - title = video.title_or_slug_or_generic_label(&translations); - site_path = video.site_path.normalized(); - } - } + fn video_js(translations: &Translations, video: &Video) -> String { + let image = video.poster + .as_ref() + .and_then(|poster| poster.assets.as_ref()) + .map(|assets| assets.max_160.target_filename("poster")); + let title = video.title_or_slug_or_generic_label(&translations); + let site_path = video.site_path.normalized(); let r_image = if let Some(file_name) = image { - format!("image: '{file_name}',") + format!("image:'{file_name}',") } else { String::new() }; let title_escaped = js_escape_inside_single_quoted_string(title); - let r_item = formatdoc!(r#" - {{ - kind: '{kind}', - {r_image} - title: '{title_escaped}', - url: '{site_path}' - }} - "#); - items.push(r_item); + format!("{{{r_image}sitePath:'{site_path}',title:'{title_escaped}'}}") + } + + fn playlist_js(playlist: &Playlist, translations: &Translations) -> String { + let title = playlist.title_or_slug_or_generic_label(&translations); + let site_path = playlist.site_path.normalized(); + + let title_escaped = js_escape_inside_single_quoted_string(title); + + let videos = playlist.public_videos_in_playlist_order() + .iter() + .map(|video| video_js(translations, &video)) + .collect::>() + .join(", "); + + format!("{{sitePath:'{site_path}',title:'{title_escaped}',videos:[{videos}]}}") + } + + fn collection_js(collection: &Collection, translations: &Translations) -> String { + let title = collection.title_or_slug_or_generic_label(&translations); + let site_path = collection.site_path.normalized(); + + let title_escaped = js_escape_inside_single_quoted_string(title); + + let playlists = collection.public_playlists_desc_by_release_date() + .iter() + .map(|playlist| playlist_js(&playlist, translations)) + .collect::>() + .join(", "); + + let subcollections = collection.public_subcollections_desc_by_release_date() + .iter() + .map(|subcollection| collection_js(&subcollection, translations)) + .collect::>() + .join(", "); + + let videos = collection.public_videos_in_collection_video_order() + .iter() + .map(|video| video_js(translations, &video)) + .collect::>() + .join(", "); + + format!("{{\ + playlists:[{playlists}],\ + sitePath:'{site_path}',\ + subcollections:[{subcollections}],\ + title:'{title_escaped}',\ + videos:[{videos}]\ + }}") + } + + let site_content = match &site.content { + SiteContent::Collection(collection) => { + let r_collection = collection_js(collection, translations); + let site_content = format!("{{collection:{r_collection}}}"); + Some(site_content) + } + SiteContent::Empty => None, + SiteContent::Invalid { .. } => None, + SiteContent::Playlist(playlist) => { + let r_playlist = playlist_js(playlist, translations); + let site_content = format!("{{playlist:{r_playlist}}}"); + Some(site_content) + } + SiteContent::Video(_) => None }; - site.walk_recursive(&mut visit); + if let Some(site_content) = site_content { + let t_nothing_found_for_xxx = js_escape_inside_single_quoted_string(translations.nothing_found_for_xxx); + let t_showing_xxx_results_for_xxx = js_escape_inside_single_quoted_string(translations.showing_xxx_results_for_xxx); + let mut js = formatdoc!(r#" + const SEARCH_JS_T = {{ + nothingFoundForXxx: query => '{t_nothing_found_for_xxx}'.replace('{{query}}', query), + showingXxxResultsForXxx: (count, query) => '{t_showing_xxx_results_for_xxx}'.replace('{{count}}', count).replace('{{query}}', query) + }}; + const siteContent = {site_content}; + "#); - let r_items = items.join(",\n"); + js.push_str(include_str!("assets/search.js")); - let t_nothing_found_for_xxx = js_escape_inside_single_quoted_string(translations.nothing_found_for_xxx); - let t_showing_xxx_results_for_xxx = js_escape_inside_single_quoted_string(translations.showing_xxx_results_for_xxx); - let mut js = formatdoc!(r#" - const SEARCH_JS_T = {{ - nothingFoundForXxx: query => '{t_nothing_found_for_xxx}'.replace('{{query}}', query), - showingXxxResultsForXxx: (count, query) => '{t_showing_xxx_results_for_xxx}'.replace('{{count}}', count).replace('{{query}}', query) - }}; + asset_hashes.search_js = Some(url_safe_hash_base64(&js)); - const SITE_ITEMS = [ - {r_items} - ]; - "#); - - js.push_str(include_str!("assets/search.js")); - - asset_hashes.search_js = Some(url_safe_hash_base64(&js)); - - fs::write( - context.build_dir.join("search.js"), - js - ).unwrap(); + fs::write( + context.build_dir.join("search.js"), + js + ).unwrap(); + } } pub fn generate_video_js(context: &Context) { diff --git a/src/editor/input.rs b/src/editor/input.rs index 6d25db3..68dc471 100644 --- a/src/editor/input.rs +++ b/src/editor/input.rs @@ -151,4 +151,4 @@ impl Display for Input<'_> { write!(formatter, "{html}") } -} \ No newline at end of file +}