mirror of
https://codeberg.org/simonrepp/hyper8.git
synced 2026-08-14 13:45:27 +02:00
Switch to a nested search model, exclude search feature on single video sites
This commit is contained in:
+137
-16
@@ -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);
|
||||
|
||||
+32
-37
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+26
-16
@@ -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#"
|
||||
<div class="search" data-root-prefix="{root_prefix}">
|
||||
<div class="icon">{search_icon}</div>
|
||||
<input autocomplete="off" placeholder="{t_search} (s)" type="search">
|
||||
<button>
|
||||
{close_icon}
|
||||
</button>
|
||||
<div class="pane">
|
||||
<div role="status"></div>
|
||||
<div class="results"></div>
|
||||
</div>
|
||||
</div>
|
||||
"#);
|
||||
search_script = format!(r#"<script defer src="{root_prefix}search.js?{search_js_asset_hash}"></script>"#);
|
||||
} else {
|
||||
search = String::new();
|
||||
search_script = String::new();
|
||||
};
|
||||
|
||||
let head = formatdoc!(r#"
|
||||
<head>
|
||||
@@ -113,7 +136,7 @@ impl Layout {
|
||||
{adaptive_theme_script}
|
||||
{clipboard_script}
|
||||
{video_script}
|
||||
<script defer src="{root_prefix}search.js?{search_js_asset_hash}"></script>
|
||||
{search_script}
|
||||
</head>
|
||||
"#);
|
||||
|
||||
@@ -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##"
|
||||
<body>
|
||||
<div class="layout">
|
||||
@@ -178,17 +198,7 @@ impl Layout {
|
||||
Hyper 8 Video System {version_display}
|
||||
</a>
|
||||
</div>
|
||||
<div class="search" data-root-prefix="{root_prefix}">
|
||||
<div class="icon">{search_icon}</div>
|
||||
<input autocomplete="off" placeholder="{t_search} (s)" type="search">
|
||||
<button>
|
||||
{close_icon}
|
||||
</button>
|
||||
<div class="pane">
|
||||
<div role="status"></div>
|
||||
<div class="results"></div>
|
||||
</div>
|
||||
</div>
|
||||
{search}
|
||||
</footer>
|
||||
</div>
|
||||
{templates}
|
||||
|
||||
@@ -112,7 +112,7 @@ pub fn playlist_html(
|
||||
.join("\n");
|
||||
|
||||
let videos_rendered = formatdoc!(r#"
|
||||
<div class="playlist">
|
||||
<div class="playlist_page">
|
||||
{videos}
|
||||
</div>
|
||||
"#);
|
||||
|
||||
+99
-64
@@ -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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
let subcollections = collection.public_subcollections_desc_by_release_date()
|
||||
.iter()
|
||||
.map(|subcollection| collection_js(&subcollection, translations))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
let videos = collection.public_videos_in_collection_video_order()
|
||||
.iter()
|
||||
.map(|video| video_js(translations, &video))
|
||||
.collect::<Vec<String>>()
|
||||
.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) {
|
||||
|
||||
+1
-1
@@ -151,4 +151,4 @@ impl Display for Input<'_> {
|
||||
|
||||
write!(formatter, "{html}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user