0.0.5s
This commit is contained in:
+115
-190
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user