update search to include video titles

This commit is contained in:
2026-05-25 10:08:32 +02:00
parent fda2ed36bd
commit f2cebddd4d
4 changed files with 218 additions and 33 deletions

View File

@@ -5423,6 +5423,35 @@ input {
white-space: nowrap;
}
/* Section headers inside the suggestion dropdown (e.g. "Tags" / "Titles") */
.tag-suggestion-header {
padding: 5px 12px 3px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
user-select: none;
}
/* Icon prefix for title-type suggestion items */
.tag-suggestion-icon {
flex-shrink: 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
margin-right: 8px;
}
.tag-suggestion-item--title .tag-suggestion-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--white, #e0e0e0);
}
@media (max-width: 555px) {
.tag-suggestions {
position: fixed;

View File

@@ -4269,16 +4269,30 @@ window.cancelAnimFrame = (function () {
}
};
const renderSuggestions = (items) => {
const renderSuggestions = (tagItems, titleItems) => {
suggestions.innerHTML = '';
highlightIdx = -1;
if (!items.length) {
const totalItems = (tagItems || []).length + (titleItems || []).length;
if (!totalItems) {
suggestions.style.display = 'none';
return;
}
items.forEach(s => {
const addSectionHeader = (label) => {
const hdr = document.createElement('div');
hdr.className = 'tag-suggestion-header';
hdr.textContent = label;
suggestions.appendChild(hdr);
};
// --- Tag results ---
if (tagItems && tagItems.length) {
if (titleItems && titleItems.length) addSectionHeader('Tags');
tagItems.forEach(s => {
const div = document.createElement('div');
div.className = 'tag-suggestion-item';
div.dataset.type = 'tag';
div.dataset.value = s.tag;
const name = document.createElement('span');
name.className = 'tag-suggestion-name';
@@ -4307,6 +4321,50 @@ window.cancelAnimFrame = (function () {
});
suggestions.appendChild(div);
});
}
// --- Title results ---
if (titleItems && titleItems.length) {
if (tagItems && tagItems.length) addSectionHeader('Titles');
titleItems.forEach(s => {
const div = document.createElement('div');
div.className = 'tag-suggestion-item tag-suggestion-item--title';
div.dataset.type = 'title';
div.dataset.id = s.id;
div.dataset.title = s.title;
const icon = document.createElement('span');
icon.className = 'tag-suggestion-icon';
icon.innerHTML = '<i class="fa-regular fa-file" aria-hidden="true"></i>';
const name = document.createElement('span');
name.className = 'tag-suggestion-name';
name.textContent = s.title.length > 60 ? s.title.substring(0, 60) + '…' : s.title;
const meta = document.createElement('span');
meta.className = 'tag-suggestion-meta';
meta.textContent = `#${s.id}`;
div.appendChild(icon);
div.appendChild(name);
div.appendChild(meta);
div.addEventListener('mousedown', (e) => {
e.preventDefault();
suggestions.style.display = 'none';
highlightIdx = -1;
toggleSearch(false);
const target = `/search/?tag=title:${encodeURIComponent(s.title)}`;
if (typeof loadPageAjax === 'function') {
loadPageAjax(target, true);
} else {
window.location.href = target;
}
});
suggestions.appendChild(div);
});
}
suggestions.style.display = 'block';
};
@@ -4321,10 +4379,14 @@ window.cancelAnimFrame = (function () {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/v2/tags/suggest?q=${encodeURIComponent(q)}`);
const json = await res.json();
if (json.success && json.suggestions) {
renderSuggestions(json.suggestions.slice(0, 8));
const [tagRes, titleRes] = await Promise.all([
fetch(`/api/v2/tags/suggest?q=${encodeURIComponent(q)}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/v2/items/suggest?q=${encodeURIComponent(q)}`).then(r => r.json()).catch(() => ({ success: false }))
]);
const tagSuggestions = (tagRes.success && tagRes.suggestions) ? tagRes.suggestions.slice(0, 6) : [];
const titleSuggestions = (titleRes.success && titleRes.suggestions) ? titleRes.suggestions.slice(0, 4) : [];
if (tagSuggestions.length || titleSuggestions.length) {
renderSuggestions(tagSuggestions, titleSuggestions);
} else {
suggestions.style.display = 'none';
}
@@ -4467,7 +4529,22 @@ window.cancelAnimFrame = (function () {
if (highlightIdx >= 0) {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (items[highlightIdx]) {
const selectedTag = items[highlightIdx].querySelector('.tag-suggestion-name').textContent;
const el = items[highlightIdx];
// Title results: go to the search page showing all items with this title
if (el.dataset.type === 'title') {
suggestions.style.display = 'none';
highlightIdx = -1;
toggleSearch(false);
const target = `/search/?tag=title:${encodeURIComponent(el.dataset.title)}`;
if (typeof loadPageAjax === 'function') {
loadPageAjax(target, true);
} else {
window.location.href = target;
}
return;
}
// Tag results: fill input
const selectedTag = el.querySelector('.tag-suggestion-name').textContent;
const isStrict = strict && strict.checked;
if (isStrict) {
const parts = input.value.split(',');
@@ -4481,7 +4558,22 @@ window.cancelAnimFrame = (function () {
return;
}
}
// Snapshot before hiding — if there are only title matches and no tag matches,
// go to the title search page instead of a futile tag search.
const tagItems = suggestions.querySelectorAll('.tag-suggestion-item[data-type="tag"]');
const titleItems = suggestions.querySelectorAll('.tag-suggestion-item[data-type="title"]');
suggestions.style.display = 'none';
if (tagItems.length === 0 && titleItems.length > 0) {
toggleSearch(false);
const q = input.value.trim();
const target = `/search/?tag=title:${encodeURIComponent(q)}`;
if (typeof loadPageAjax === 'function') {
loadPageAjax(target, true);
} else {
window.location.href = target;
}
return;
}
doSearch();
}
});

View File

@@ -767,6 +767,28 @@ export default router => {
}
});
group.get(/\/items\/suggest$/, async (req, res) => {
const searchString = req.url.qs.q;
if (!searchString || searchString.length < 1) {
return res.json({ success: false, suggestions: [] });
}
try {
const items = await db`
SELECT id, title
FROM items
WHERE title IS NOT NULL
AND active = true
AND title ILIKE ${'%' + searchString + '%'}
ORDER BY id DESC
LIMIT 8
`;
return res.json({ success: true, suggestions: items });
} catch (err) {
return res.json({ success: false, error: 'Item title suggestion error', suggestions: [] });
}
});
// tags lol
group.put(/\/tags\/rename\/(?<tagname>.*)/, lib.modAuth, async (req, res) => {

View File

@@ -56,6 +56,48 @@ export default (router, tpl) => {
path: '&page='
};
}
else if (tag.startsWith('title:')) {
const titleQuery = tag.substring(6).trim();
const q = '%' + titleQuery + '%';
total = (await db`
select count(*) as total
from "items"
where title ilike ${q} and active = true
`)[0]?.total ?? 0;
total = +total;
const pages = +Math.ceil(total / _eps);
const act_page = Math.min(Math.max(pages, 1), page || 1);
const offset = Math.max(0, (act_page - 1) * _eps);
ret = await db`
select *
from "items"
where title ilike ${q} and active = true
order by id desc
offset ${offset}
limit ${_eps}
`;
const cheat = [];
for (let i = Math.max(1, act_page - 3); i <= Math.min(act_page + 3, pages); i++)
cheat.push(i);
pagination = {
start: 1,
end: pages,
prev: (act_page > 1) ? act_page - 1 : null,
next: (act_page < pages) ? act_page + 1 : null,
page: act_page,
cheat: cheat,
uff: false
};
link = {
main: `/search/?tag=${encodeURIComponent(tag)}`,
path: '&page='
};
}
else if (mode === 'strict') {
const tags = tag.split(',').map(t => t.trim()).filter(t => t.length > 0);