update search to include video titles
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user