Files
Local-Booru/templates/index.html
T
Hell13Cat 7b86090b75 0.0.10s
2026-06-02 01:17:25 +03:00

280 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LocalBooru</title>
<style>
:root {
--bg-main: #1e1e1e; --bg-header: #252525; --bg-card: #2a2a2a;
--bg-panel: #222; --bg-input: #333; --text-main: #eee;
--text-muted: #aaa; --border-color: #444;
--color-g: #4caf50; --color-s: #ff9800; --color-q: #ffeb3b; --color-e: #f44336;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: var(--bg-main); color: var(--text-main); font-family: sans-serif; font-size: 14px; }
header { display: flex; align-items: center; background-color: var(--bg-header); padding: 10px 20px; border-bottom: 1px solid var(--border-color); gap: 15px; flex-wrap: wrap; }
.logo { font-weight: bold; font-size: 18px; color: white; text-decoration: none; }
.analytics { color: #88bece; text-decoration: none; font-size: 12px; }
.search-wrapper { flex-grow: 1; max-width: 400px; position: relative; }
.search-wrapper input { width: 100%; background-color: var(--bg-input); border: 1px solid var(--border-color); color: white; padding: 6px 10px; border-radius: 3px; outline: none; }
.search-wrapper input:focus { border-color: var(--text-muted); }
.autocomplete-list { position: absolute; top: 100%; left: 0; right: 0; background-color: var(--bg-card); border: 1px solid var(--border-color); border-radius: 3px; margin-top: 4px; list-style: none; z-index: 1000; max-height: 250px; overflow-y: auto; box-shadow: 0 4px 10px rgba(0,0,0,0.5); display: none; }
.autocomplete-item { padding: 8px 10px; cursor: pointer; display: flex; justify-content: space-between; border-bottom: 1px solid #333; }
.autocomplete-item:last-child { border-bottom: none; }
.autocomplete-item:hover { background-color: var(--bg-input); }
.autocomplete-count { color: var(--text-muted); font-size: 12px; }
.rating-filters { display: flex; gap: 5px; }
.rating-btn { background: transparent; color: white; border: 1px solid; width: 24px; height: 24px; border-radius: 3px; cursor: pointer; font-size: 12px; font-weight: bold; }
.rating-btn.active { opacity: 1; }
.rating-btn:not(.active) { opacity: 0.3; }
#btn-e { border-color: var(--color-e); color: var(--color-e); }
#btn-q { border-color: var(--color-q); color: var(--color-q); }
#btn-s { border-color: var(--color-s); color: var(--color-s); }
#btn-g { border-color: var(--color-g); color: var(--color-g); }
main { padding: 20px; max-width: 1600px; margin: 0 auto; }
.results-header { font-size: 18px; font-weight: bold; margin-bottom: 20px; }
/* Pagination */
.pagination-container { display: flex; justify-content: center; align-items: center; gap: 10px; margin: 20px 0; }
.pagination-btn { background-color: var(--bg-header); border: 1px solid var(--border-color); color: var(--text-muted); padding: 5px 12px; cursor: pointer; border-radius: 3px; }
.pagination-btn:hover:not(:disabled) { background-color: var(--bg-input); color: white; }
.pagination-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
.card { background-color: var(--bg-card); display: flex; flex-direction: column; border-radius: 3px; overflow: hidden; text-decoration: none; color: inherit; transition: transform 0.1s;}
.card:hover { transform: translateY(-2px); }
.card-image-container { width: 100%; aspect-ratio: 1/1; background-color: #333; position: relative; }
.card-image-container img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-meta { padding: 6px; text-align: center; font-size: 10px; color: var(--text-muted); }
.loader { text-align: center; padding: 50px; color: var(--text-muted); grid-column: 1/-1; }
@media (max-width: 600px) { .search-wrapper { order: 3; max-width: 100%; width: 100%; margin-top: 10px; } }
</style>
</head>
<body>
<header>
<a href="/" class="logo">LocalBooru</a>
<!--
<a class="analytics" href="/analytics">📊 Analytics</a>
-->
<div class="search-wrapper">
<input type="text" id="searchInput" placeholder="Search tags...">
<ul id="autocompleteList" class="autocomplete-list"></ul>
</div>
<div class="rating-filters">
<button id="btn-e" class="rating-btn active" data-rating="e">E</button>
<button id="btn-q" class="rating-btn active" data-rating="q">Q</button>
<button id="btn-s" class="rating-btn active" data-rating="s">S</button>
<button id="btn-g" class="rating-btn active" data-rating="g">G</button>
</div>
</header>
<main>
<div class="results-header" id="resultsCount">Results (0)</div>
<div class="pagination-container" id="paginationTop"></div>
<div class="grid" id="imageGrid"></div>
<div class="pagination-container" id="paginationBottom"></div>
</main>
<script>
const state = {
page: 0,
query: "",
ratings: { e: true, q: true, s: true, g: true },
totalPages: 1,
totalResults: 0
};
function init() {
setupAutocomplete('searchInput', 'autocompleteList');
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('autocompleteList').style.display = 'none';
state.query = searchInput.value;
state.page = 0;
fetchData();
}
});
document.querySelectorAll('.rating-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const r = e.target.getAttribute('data-rating');
state.ratings[r] = !state.ratings[r];
e.target.classList.toggle('active', state.ratings[r]);
state.page = 0;
fetchData();
});
});
const urlQuery = new URLSearchParams(window.location.search).get('query');
if (urlQuery) {
state.query = urlQuery;
searchInput.value = urlQuery;
}
fetchData();
}
// --- АВТОДОПОЛНЕНИЕ ---
function setupAutocomplete(inputId, listId) {
const input = document.getElementById(inputId);
const list = document.getElementById(listId);
let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
const words = input.value.split(' ');
const currentWord = words[words.length - 1];
if (currentWord.length < 2) {
list.style.display = 'none';
return;
}
debounceTimer = setTimeout(async () => {
const req = { type: "autocomplete", q: currentWord };
try {
const response = await fetch('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req)
});
if (!response.ok) throw new Error('Ошибка API');
const data = await response.json();
if (!data || data.length === 0) {
list.style.display = 'none';
return;
}
list.innerHTML = data.map(item => `
<li class="autocomplete-item" data-tag="${item.tag}">
<span>${item.tag}</span><span class="autocomplete-count">${item.count}</span>
</li>
`).join('');
list.style.display = 'block';
list.querySelectorAll('.autocomplete-item').forEach(li => {
li.addEventListener('click', () => {
words[words.length - 1] = li.getAttribute('data-tag');
input.value = words.join(' ') + ' ';
list.style.display = 'none';
input.focus();
});
});
} catch (error) {
console.error(error);
}
}, 200);
});
document.addEventListener('click', (e) => {
if (e.target !== input && e.target !== list) list.style.display = 'none';
});
}
// --- ПОИСК И ВЫВОД РЕЗУЛЬТАТОВ ---
async function fetchData() {
const grid = document.getElementById('imageGrid');
grid.innerHTML = `<div class="loader">Загрузка...</div>`;
const activeRatings = Object.keys(state.ratings).filter(k => state.ratings[k]).join('+');
const req = {
type: "search",
page: state.page,
query: state.query.trim(),
rating: activeRatings
};
try {
const response = await fetch('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req)
});
if (!response.ok) throw new Error('Ошибка сервера');
const data = await response.json();
// Синхронизация состояния: вычисляем количество страниц (Math.ceil(всего / элементов_на_странице))
state.totalResults = data.results_info.total;
state.page = data.results_info.page;
const itemsPerPage = data.results_info.count_page || 1; // защита от деления на 0
state.totalPages = Math.ceil(state.totalResults / itemsPerPage);
document.getElementById('resultsCount').textContent = `Results (${state.totalResults})`;
if (data.results.length === 0) {
grid.innerHTML = `<div class="loader">Ничего не найдено</div>`;
} else {
grid.innerHTML = data.results.map(i => `
<a href="/post/${i.md5}" class="card">
<div class="card-image-container">
<img src="/thumb/${i.md5}.jpg" alt="thumbnail" loading="lazy">
</div>
<div class="card-meta">
Date: ${i.display_date[1]}<br>
Score: ${i.score} | Rating: <span style="color:var(--color-${i.rating[0].toLowerCase()})">${i.rating}</span>
</div>
</a>
`).join('');
}
renderPagination();
} catch (err) {
console.error(err);
grid.innerHTML = `<div class="loader">Ошибка связи с API</div>`;
}
}
// --- ПАГИНАЦИЯ ---
function renderPagination() {
const totalPages = Math.max(0, state.totalPages);
const currentPage = state.page;
const hasPrev = currentPage > 0;
const hasNext = currentPage < totalPages - 1;
const displayPage = totalPages === 0 ? 1 : currentPage + 1;
const displayTotal = totalPages === 0 ? 1 : totalPages;
const html = `
<button class="pagination-btn" onclick="changePage(0)" ${!hasPrev ? 'disabled' : ''}>&laquo; First</button>
<button class="pagination-btn" onclick="changePage(${currentPage - 1})" ${!hasPrev ? 'disabled' : ''}>&lsaquo; Prev</button>
<span style="font-size:12px; font-weight:bold;">Page ${displayPage} of ${displayTotal}</span>
<button class="pagination-btn" onclick="changePage(${currentPage + 1})" ${!hasNext ? 'disabled' : ''}>Next &rsaquo;</button>
<button class="pagination-btn" onclick="changePage(${totalPages - 1})" ${!hasNext ? 'disabled' : ''}>Last &raquo;</button>
`;
document.getElementById('paginationTop').innerHTML = html;
document.getElementById('paginationBottom').innerHTML = html;
}
// Смена страницы
window.changePage = (newPage) => {
if (newPage < 0 || (state.totalPages > 0 && newPage >= state.totalPages)) return;
state.page = newPage;
fetchData();
window.scrollTo(0, 0);
};
init();
</script>
</body>
</html>