192 lines
10 KiB
HTML
192 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Post - LocalBooru</title>
|
|
<style>
|
|
:root {
|
|
--bg-main: #1e1e1e; --bg-header: #252525; --bg-panel: #2a2a2a; --bg-input: #333;
|
|
--text-main: #eee; --text-muted: #aaa; --border-color: #444; --link-color: #4da8da;
|
|
--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; }
|
|
.logo { font-weight: bold; font-size: 18px; color: white; text-decoration: none; }
|
|
|
|
.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; }
|
|
.autocomplete-list { position: absolute; top: 100%; left: 0; right: 0; background-color: var(--bg-panel); 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; }
|
|
|
|
main { padding: 20px; max-width: 1600px; margin: 0 auto; display: grid; grid-template-columns: 300px 1fr; gap: 20px; align-items: start; }
|
|
.sidebar { background-color: var(--bg-panel); border: 1px solid var(--border-color); border-radius: 4px; padding: 15px; display: flex; flex-direction: column; gap: 20px; }
|
|
.section-title { font-size: 16px; font-weight: bold; border-bottom: 1px solid var(--border-color); padding-bottom: 8px; margin-bottom: 10px; }
|
|
.stat-row { margin-bottom: 6px; word-break: break-all; line-height: 1.4;}
|
|
.stat-label { font-weight: bold; }
|
|
.tags-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.tags-list a { color: var(--link-color); text-decoration: none; }
|
|
.tags-list a:hover { text-decoration: underline; }
|
|
.media-container { background-color: var(--bg-panel); border: 1px solid var(--border-color); border-radius: 4px; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 400px; }
|
|
.media-container img, .media-container video { max-width: 100%; max-height: 85vh; object-fit: contain; border-radius: 2px; }
|
|
.loader { grid-column: 1/-1; text-align: center; padding: 50px; color: var(--text-muted); }
|
|
|
|
@media (max-width: 900px) { main { grid-template-columns: 1fr; } .media-container { order: -1; } header { flex-wrap: wrap; } .search-wrapper { min-width: 100%; order: 3; margin-top: 10px; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<a href="/" class="logo">LocalBooru</a>
|
|
<div class="search-wrapper">
|
|
<input type="text" id="searchInput" placeholder="Search tags...">
|
|
<ul id="autocompleteList" class="autocomplete-list"></ul>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="mainContent">
|
|
<div class="loader">Загрузка поста...</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Инициализация автодополнения
|
|
const searchInput = document.getElementById('searchInput');
|
|
const list = document.getElementById('autocompleteList');
|
|
let debounceTimer;
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
clearTimeout(debounceTimer);
|
|
const words = searchInput.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');
|
|
searchInput.value = words.join(' ') + ' ';
|
|
list.style.display = 'none';
|
|
searchInput.focus();
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error("Автодополнение недоступно:", error);
|
|
}
|
|
}, 200);
|
|
});
|
|
|
|
// Скрытие списка автодополнения при клике вне него
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target !== searchInput && e.target !== list) {
|
|
list.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Переход на главную при поиске
|
|
searchInput.addEventListener('keypress', (e) => {
|
|
if(e.key === 'Enter') {
|
|
window.location.href = `index.html?query=${encodeURIComponent(searchInput.value.trim())}`;
|
|
}
|
|
});
|
|
|
|
// Загрузка данных поста
|
|
async function fetchPost() {
|
|
// Получаем ID из параметра ?id=... или из конца URL пути
|
|
let postId = new URLSearchParams(window.location.search).get('id') || window.location.pathname.split('/').pop();
|
|
|
|
if (!postId || postId.includes('.html')) {
|
|
document.getElementById('mainContent').innerHTML = `<div class="loader">ID поста не указан</div>`;
|
|
return;
|
|
}
|
|
|
|
const req = { type: "post", post_id: postId };
|
|
|
|
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();
|
|
|
|
// Рендер медиа (предполагается, что оригиналы лежат в /img/)
|
|
let mediaHtml = '';
|
|
const mediaUrl = `/file/${data.local_filename}`;
|
|
|
|
if (data.is_video) {
|
|
mediaHtml = `<video controls autoplay loop src="${mediaUrl}"></video>`;
|
|
} else {
|
|
// И для изображений, и для GIF подойдет тег <img>
|
|
mediaHtml = `<img src="${mediaUrl}" alt="Post Media">`;
|
|
}
|
|
|
|
// Рендер тегов
|
|
const tagsHtml = data.tags.map(t => `<a href="/?query=${encodeURIComponent(t)}">${t}</a>`).join('');
|
|
|
|
// Рендер всей страницы
|
|
document.getElementById('mainContent').innerHTML = `
|
|
<aside class="sidebar">
|
|
<div class="statistics">
|
|
<div class="section-title">Statistics</div>
|
|
<div class="stat-row"><span class="stat-label">File:</span> ${data.local_filename}</div>
|
|
<div class="stat-row"><span class="stat-label">Date:</span> ${data.display_date[1]}</div>
|
|
<div class="stat-row"><span class="stat-label">Rating:</span> <span style="color:var(--color-${data.rating[0].toLowerCase()})">${data.rating}</span></div>
|
|
<div class="stat-row"><span class="stat-label">Score:</span> ${data.score}</div>
|
|
<div class="stat-row"><span class="stat-label">Resolution:</span> ${data.width}x${data.height}</div>
|
|
<div class="stat-row"><span class="stat-label">Source:</span> <a href="${data.source}" target="_blank" rel="noopener noreferrer" style="color: var(--link-color); text-decoration: none;">Link</a></div>
|
|
</div>
|
|
<div class="tags">
|
|
<div class="section-title">Tags</div>
|
|
<div class="tags-list">${tagsHtml}</div>
|
|
</div>
|
|
</aside>
|
|
<div class="media-container">
|
|
${mediaHtml}
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
console.error(err);
|
|
document.getElementById('mainContent').innerHTML = `<div class="loader">Ошибка загрузки поста. Проверьте соединение с сервером.</div>`;
|
|
}
|
|
}
|
|
|
|
// Вызов функции при старте
|
|
fetchPost();
|
|
</script>
|
|
</body>
|
|
</html> |