This commit is contained in:
Hell13Cat
2026-05-27 01:28:03 +03:00
parent ed7bc4fba5
commit 2a103d246b
9 changed files with 383 additions and 605 deletions
+115 -190
View File
@@ -6,104 +6,42 @@
<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;
--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; }
body {
background-color: var(--bg-main);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, 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;
}
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 & Autocomplete */
.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-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;
}
.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;
}
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; line-height: 1.4; word-break: break-all;}
.stat-row { margin-bottom: 6px; word-break: break-all; line-height: 1.4;}
.stat-label { font-weight: bold; }
.stat-val-g { color: var(--color-g); }
.stat-val-s { color: var(--color-s); }
.stat-val-q { color: var(--color-q); }
.stat-val-e { color: var(--color-e); }
.tags-list { display: flex; flex-direction: column; gap: 6px; }
.tags-list a { color: var(--link-color); text-decoration: none; word-break: break-word; }
.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 { 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); }
.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; }
}
@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="index.html" class="logo">LocalBooru</a>
<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>
@@ -111,11 +49,11 @@
</header>
<main id="mainContent">
<div class="loader" id="loader">Загрузка поста...</div>
<div class="loader">Загрузка поста...</div>
</main>
<script>
// Инициализация поиска с автодополнением
// Инициализация автодополнения
const searchInput = document.getElementById('searchInput');
const list = document.getElementById('autocompleteList');
let debounceTimer;
@@ -125,142 +63,129 @@
const words = searchInput.value.split(' ');
const currentWord = words[words.length - 1];
if (currentWord.length < 2) { list.style.display = 'none'; return; }
if (currentWord.length < 2) {
list.style.display = 'none';
return;
}
debounceTimer = setTimeout(async () => {
const req = { type: "autocomplete", q: currentWord };
// РЕАЛЬНОЕ API: const data = await fetch('/api', ...);
const data = await mockAutocompleteAPI(req);
if (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();
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';
if (e.target !== searchInput && e.target !== list) {
list.style.display = 'none';
}
});
// Переход на главную при нажатии Enter
// Переход на главную при поиске
searchInput.addEventListener('keypress', (e) => {
if(e.key === 'Enter') window.location.href = `index.html?query=${searchInput.value.trim()}`;
if(e.key === 'Enter') {
window.location.href = `index.html?query=${encodeURIComponent(searchInput.value.trim())}`;
}
});
// Загрузка поста
// Загрузка данных поста
async function fetchPost() {
let postId = new URLSearchParams(window.location.search).get('id');
if (!postId) postId = window.location.pathname.split('/').pop();
// Получаем 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>`;
document.getElementById('mainContent').innerHTML = `<div class="loader">ID поста не указан</div>`;
return;
}
const requestBody = { type: "post", post_id: postId };
try {
// РЕАЛЬНОЕ API: fetch('/api', ...);
const data = await generateMockPostData(requestBody);
renderPost(data);
} catch (error) {
document.getElementById('mainContent').innerHTML = `<div class="loader">Ошибка загрузки</div>`;
}
}
function renderPost(data) {
const main = document.getElementById('mainContent');
const req = { type: "post", post_id: postId };
let mediaHtml = '';
if (data.media_type === 'video') {
mediaHtml = `<video controls autoplay loop src="${data.media_url}"></video>`;
} else {
mediaHtml = `<img src="${data.media_url}" alt="Post Media">`;
}
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(tag => `<a href="index.html?query=${tag}">${tag}</a>`).join('');
// Рендер тегов
const tagsHtml = data.tags.map(t => `<a href="/?query=${encodeURIComponent(t)}">${t}</a>`).join('');
main.innerHTML = `
<aside class="sidebar">
<div class="statistics">
<div class="section-title">Statistics</div>
<div class="stat-row"><span class="stat-label">File:</span> ${data.file_name}</div>
<div class="stat-row"><span class="stat-label">Date:</span> ${data.date}</div>
<div class="stat-row"><span class="stat-label">Rating:</span> <span class="stat-val-${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">Size:</span> ${data.size}</div>
<div class="stat-row"><span class="stat-label">Source:</span> <a href="${data.source}" 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}
// Рендер всей страницы
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>
</aside>
<div class="media-container">
${mediaHtml}
</div>
`;
}
// --- ВРЕМЕННЫЕ ЗАГЛУШКИ ДЛЯ API ---
function mockAutocompleteAPI(req) {
return new Promise(res => {
const db = [
{tag: "long_hair", count: 8184}, {tag: "long_sleeves", count: 2828},
{tag: "looking_at_viewer", count: 8062}, {tag: "1girl", count: 11061},
{tag: "blue_hair", count: 3279}, {tag: "blue_eyes", count: 3279}
];
res(db.filter(t => t.tag.includes(req.q.toLowerCase())));
});
}
function generateMockPostData(req) {
return new Promise(resolve => {
setTimeout(() => {
resolve({
post_id: req.post_id,
file_name: `${req.post_id}.jpg`,
date: "2026-05-24",
rating: "general",
score: 0,
size: "3916x3062",
source: "#",
media_type: "image",
media_url: "data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22600%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23444%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20fill%3D%22%23fff%22%20font-size%3D%2224%22%20text-anchor%3D%22middle%22%3E%D0%9F%D0%BB%D0%B5%D0%B9%D1%81%D1%85%D0%BE%D0%BB%D0%B4%D0%B5%D1%80%3C%2Ftext%3E%3C%2Fsvg%3E",
tags: [
"1boy", "absurdres", "arknights", "blue_hair", "blue_neckerchief",
"commentary", "english_commentary", "gloves", "hat", "highres",
"holding", "holding_umbrella", "infection_monitor_(arknights)",
"looking_at_viewer", "male_focus", "mizuki_(arknights)", "neckerchief",
"pink_eyes", "pppmepl", "rain", "smile", "solo", "teruterubouzu",
"trap", "umbrella", "upper_body"
]
});
}, 300);
});
`;
} catch (err) {
console.error(err);
document.getElementById('mainContent').innerHTML = `<div class="loader">Ошибка загрузки поста. Проверьте соединение с сервером.</div>`;
}
}
// Вызов функции при старте
fetchPost();
</script>
</body>