289 lines
14 KiB
Django/Jinja
289 lines
14 KiB
Django/Jinja
{% extends "base.html.j2" %}
|
|
<!-- Side quest 17/21: Every hash tells a story. -->
|
|
{% block title %}{{ title }} - neopig{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="/static/vendor/highlight-github-dark.min.css">
|
|
<script src="/static/vendor/highlight.min.js"></script>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
h1 { color: #ff6b6b; font-size: 20px; margin: 0 0 10px 0; }
|
|
h3 { color: #ff6b6b; font-size: 16px; margin: 30px 0 15px 0; border-bottom: 1px solid #333; padding-bottom: 8px; }
|
|
.meta { background: #1a1a1a; padding: 15px; border-radius: 8px; margin: 15px 0; }
|
|
.meta-row { display: flex; margin: 8px 0; }
|
|
.meta-label { width: 100px; color: #888; font-size: 13px; }
|
|
.meta-value { flex: 1; word-break: break-all; font-size: 13px; }
|
|
.meta-value a { color: #4ade80; }
|
|
.hero { text-align: center; margin-bottom: 20px; }
|
|
.hero img, .hero video { max-width: 100%; max-height: 60vh; border-radius: 8px; }
|
|
.content-rendered {
|
|
background: #1a1a1a; padding: 20px; border-radius: 8px;
|
|
line-height: 1.7; font-size: 14px; color: #ccc; text-align: left;
|
|
}
|
|
.content-rendered img { max-width: 100%; height: auto; margin: 10px 0; }
|
|
.content-rendered img.avatar,
|
|
.content-rendered img[alt="avatar"] {
|
|
display: inline-block; vertical-align: middle;
|
|
width: 40px; height: 40px; border-radius: 50%;
|
|
margin: 0 10px 0 0; object-fit: cover;
|
|
}
|
|
.content-rendered img.emoji,
|
|
.content-rendered img[alt^=":"][alt$=":"],
|
|
.content-rendered img[src*="emoji"] {
|
|
display: inline; width: 20px; height: 20px;
|
|
max-width: 20px; margin: 0 2px; vertical-align: text-bottom;
|
|
}
|
|
.content-rendered a { color: #ff6b6b; }
|
|
.content-rendered pre, .content-rendered code {
|
|
background: #252525; padding: 2px 6px;
|
|
border-radius: 4px; font-family: monospace; font-size: 13px;
|
|
}
|
|
.content-rendered pre {
|
|
padding: 15px; display: block;
|
|
white-space: pre-wrap; overflow-x: auto;
|
|
}
|
|
.content-rendered blockquote {
|
|
background: #151520; border-left: 3px solid #4a9eff;
|
|
padding: 12px 16px; margin: 16px 0;
|
|
border-radius: 0 6px 6px 0; color: #aaa; font-style: italic;
|
|
}
|
|
.content-rendered hr { border: none; border-top: 1px solid #333; margin: 24px 0; }
|
|
.content-rendered p { margin: 0 0 16px 0; line-height: 1.7; }
|
|
.content-rendered h1, .content-rendered h2, .content-rendered h3, .content-rendered h4, .content-rendered h5, .content-rendered h6 {
|
|
display: block; color: #ff6b6b; margin-top: 28px; margin-bottom: 12px;
|
|
}
|
|
.content-rendered h1 { font-size: 1.8em; }
|
|
.content-rendered h2 { font-size: 1.5em; }
|
|
.content-rendered h3 { font-size: 1.25em; }
|
|
.media-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
gap: 10px; margin-top: 15px;
|
|
}
|
|
.media-card {
|
|
background: #1a1a1a; border-radius: 8px;
|
|
overflow: hidden; display: block; transition: transform 0.2s;
|
|
text-decoration: none;
|
|
}
|
|
.media-card:hover { transform: scale(1.02); }
|
|
.media-card img, .media-card video, .media-card svg {
|
|
width: 100%; height: 120px;
|
|
object-fit: contain; background: #0a0a0a;
|
|
}
|
|
.media-card-name {
|
|
padding: 4px 8px; font-size: 11px; color: #888;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
background: #111; text-align: left; text-decoration: none;
|
|
}
|
|
.tag { background: #333; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-right: 5px; }
|
|
.screenshot-stack { border-radius: 8px; }
|
|
.screenshot-stack img { border-radius: 0; }
|
|
.screenshot-stack img:first-child { border-radius: 8px 8px 0 0; }
|
|
.screenshot-stack img:last-child { border-radius: 0 0 8px 8px; }
|
|
.screenshot-stack img:only-child { border-radius: 8px; }
|
|
#screenshot-container.collapsed { display: none; }
|
|
.hero-grid { display: grid; grid-template-columns: 1fr; gap: 20px; margin-bottom: 20px; }
|
|
.hero-grid.has-hero { grid-template-columns: 1fr 1fr; }
|
|
.content-grid { display: grid; grid-template-columns: 1fr; gap: 20px; margin-top: 20px; }
|
|
.content-grid.has-screenshots { grid-template-columns: 1fr 1fr; }
|
|
.content-col, .screenshot-col { min-width: 0; overflow: hidden; }
|
|
.content-grid.ss-collapsed { grid-template-columns: 1fr 20px; }
|
|
.content-grid.ss-collapsed .screenshot-col h3 { writing-mode: vertical-rl; text-orientation: mixed; margin: 0; font-size: 12px; }
|
|
.sources-section { margin-top: 40px; padding-top: 20px; border-top: 1px solid #333; }
|
|
.sources-section h3 { cursor: pointer; user-select: none; }
|
|
.sources-section h3:hover { color: #ff6b6b; }
|
|
.sources-list { display: none; margin-top: 15px; }
|
|
.sources-list.expanded { display: block; }
|
|
.sources-list table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.sources-list th, .sources-list td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #333; }
|
|
.sources-list th { background: #1a1a1a; color: #888; font-weight: normal; }
|
|
.sources-list td a { color: #6ea8fe; }
|
|
.sources-list td a:hover { color: #ff6b6b; }
|
|
/* Forum post styling */
|
|
.post-block { display: flex; gap: 15px; margin: 20px 0; padding: 15px; background: #151515; border-radius: 8px; border-left: 3px solid #ff6b6b; }
|
|
.post-avatar-col { flex-shrink: 0; }
|
|
.post-avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
|
|
.post-avatar-placeholder { width: 48px; height: 48px; border-radius: 50%; background: #333; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #888; font-size: 16px; }
|
|
.post-content { flex: 1; min-width: 0; }
|
|
.post-header { margin-bottom: 10px; }
|
|
.post-header strong { color: #ff6b6b; font-size: 14px; }
|
|
.post-header .post-link { color: #555; text-decoration: none; margin-left: 8px; font-size: 12px; opacity: 0; transition: opacity 0.2s; }
|
|
.post-block:hover .post-link { opacity: 1; }
|
|
.post-link:hover { color: #ff6b6b; }
|
|
.post-highlighted { animation: highlight-fade 8s ease-out; }
|
|
@keyframes highlight-fade { 0% { background: #ff6b6b33; } 100% { background: transparent; } }
|
|
.post-content p { margin: 8px 0; }
|
|
.post-content img:not(.post-avatar):not(.emoji) { max-width: 100%; border-radius: 4px; margin: 10px 0; }
|
|
@media (max-width: 768px) { .hero-grid, .hero-grid.has-hero, .content-grid, .content-grid.has-screenshots { grid-template-columns: 1fr; } }
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
{% include 'partials/search_box.html.j2' %}
|
|
|
|
<div class="hero-grid{% if hero_html %} has-hero{% endif %}">
|
|
{% if hero_html %}
|
|
<div class="hero-left">
|
|
<div style="background:#111;border-radius:12px;padding:20px;text-align:center;">
|
|
{{ hero_html|safe }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="hero-right" style="background:#151515;border-radius:12px;padding:20px;">
|
|
<h1>{{ title }}</h1>
|
|
<div class="meta">
|
|
{% for key, value in meta_rows %}
|
|
<div class="meta-row">
|
|
<span class="meta-label">{{ t.get(key, key) }}:</span>
|
|
<span class="meta-value">{{ value|safe }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if download_btn_html %}
|
|
{{ download_btn_html|safe }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if media_items %}
|
|
<div class="page-media">
|
|
<h3>{{ t.media }} ({{ media_items|length }})</h3>
|
|
<div class="media-grid">
|
|
{% for m in media_items %}
|
|
<a href="/view/{{ m.md5_hash }}" class="media-card">
|
|
{% if m.media_type == 'video' %}
|
|
<video src="/media/{{ m.md5_hash }}" muted loop preload="metadata" onmouseenter="this.play()" onmouseleave="this.pause()"></video>
|
|
{% elif m.media_type == 'code' %}
|
|
<svg viewBox="0 0 100 100" style="width:100%;height:100%;background:#1a1a1a;border-radius:4px;"><text x="50" y="40" text-anchor="middle" fill="#6af" font-size="24">{ }</text><text x="50" y="65" text-anchor="middle" fill="#888" font-family="monospace" font-size="14" font-weight="bold">{{ m.ext or '.js' }}</text></svg>
|
|
{% elif m.media_type == 'style' %}
|
|
<svg viewBox="0 0 100 100" style="width:100%;height:100%;background:#1a1a1a;border-radius:4px;"><text x="50" y="40" text-anchor="middle" fill="#f6a" font-size="24">#</text><text x="50" y="65" text-anchor="middle" fill="#888" font-family="monospace" font-size="14" font-weight="bold">.css</text></svg>
|
|
{% elif m.media_type == 'font' %}
|
|
<svg viewBox="0 0 100 100" style="width:100%;height:100%;background:#1a1a1a;border-radius:4px;"><text x="50" y="40" text-anchor="middle" fill="#af6" font-size="24">Aa</text><text x="50" y="65" text-anchor="middle" fill="#888" font-family="monospace" font-size="14" font-weight="bold">{{ m.ext or '.woff' }}</text></svg>
|
|
{% else %}
|
|
<img src="/media/{{ m.md5_hash }}" loading="lazy">
|
|
{% endif %}
|
|
{% if m.filename %}
|
|
<div class="media-card-name">{{ m.filename }}</div>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="content-grid{% if screenshot_hashes %} has-screenshots{% endif %}">
|
|
<div class="content-col">
|
|
{% if content_html %}
|
|
<h3>{{ t.content or 'Content' }}</h3>
|
|
<div class="content-rendered">{{ content_html|safe }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if screenshot_hashes %}
|
|
<div class="screenshot-col">
|
|
<h3 onclick="toggleScreenshots()" style="cursor:pointer;">{{ t.screenshots or 'Screenshots' }} ({{ screenshot_hashes|length }}) <span id="ss-toggle" style="font-size:12px;color:#888;">[{{ t.hide }}]</span></h3>
|
|
<div class="screenshot-stack" id="screenshot-container">
|
|
{% for hash in screenshot_hashes %}
|
|
<a href="/view/{{ hash }}"><img src="/media/{{ hash }}" loading="lazy"></a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{{ sources_html|safe }}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const T = {{ t_json|safe }};
|
|
if (typeof hljs !== 'undefined') hljs.highlightAll();
|
|
|
|
function toggleScreenshots() {
|
|
const container = document.getElementById('screenshot-container');
|
|
const grid = document.querySelector('.content-grid');
|
|
const toggle = document.getElementById('ss-toggle');
|
|
const collapsed = container.classList.toggle('collapsed');
|
|
grid.classList.toggle('ss-collapsed');
|
|
toggle.textContent = collapsed ? '[' + T.show + ']' : '[' + T.hide + ']';
|
|
}
|
|
|
|
// Forum post restructuring with anchor IDs
|
|
(function() {
|
|
const container = document.querySelector('.content-rendered');
|
|
if (!container) return;
|
|
const postStarts = [];
|
|
container.querySelectorAll('p').forEach(p => {
|
|
const img = p.querySelector('img[alt="avatar"]');
|
|
const strong = p.querySelector('strong');
|
|
if (img && strong) {
|
|
postStarts.push({p, img, username: strong.textContent});
|
|
return;
|
|
}
|
|
const text = p.textContent.trim();
|
|
if (text.startsWith('avatar ') && strong) {
|
|
postStarts.push({p, img: null, username: strong.textContent});
|
|
return;
|
|
}
|
|
const avatarImg = p.querySelector('img.avatar');
|
|
if (avatarImg && strong) {
|
|
postStarts.push({p, img: avatarImg, username: strong.textContent});
|
|
}
|
|
});
|
|
if (!postStarts.length) return;
|
|
postStarts.forEach(({p, img, username}, idx) => {
|
|
if (p.closest('.post-block')) return;
|
|
const block = document.createElement('div');
|
|
block.className = 'post-block';
|
|
// Only add ID if original post element doesn't have one
|
|
const existingId = p.id || p.closest('[id]')?.id;
|
|
block.id = existingId || `post-${idx}`;
|
|
const avatarCol = document.createElement('div');
|
|
avatarCol.className = 'post-avatar-col';
|
|
if (img && img.src && !img.src.includes('undefined') && !img.src.endsWith('#') && img.getAttribute('src') !== '#') {
|
|
const avatar = document.createElement('img');
|
|
avatar.className = 'post-avatar';
|
|
avatar.src = img.src;
|
|
avatarCol.appendChild(avatar);
|
|
} else {
|
|
const ph = document.createElement('div');
|
|
ph.className = 'post-avatar-placeholder';
|
|
ph.textContent = username.slice(0, 2).toUpperCase();
|
|
avatarCol.appendChild(ph);
|
|
}
|
|
const contentCol = document.createElement('div');
|
|
contentCol.className = 'post-content';
|
|
const postId = block.id;
|
|
const postUrl = `${location.pathname}${location.search}#${postId}`;
|
|
contentCol.innerHTML = `<div class="post-header"><strong>${username}</strong> <a href="#${postId}" class="post-link" title="Link to this post" onclick="navigator.clipboard.writeText('${location.origin}${postUrl}');this.textContent='✓';setTimeout(()=>this.textContent='#',1500);return false;">#</a></div>`;
|
|
let sib = p.nextElementSibling;
|
|
const nextP = idx < postStarts.length - 1 ? postStarts[idx + 1].p : null;
|
|
while (sib && sib !== nextP && sib.tagName !== 'HR') {
|
|
contentCol.appendChild(sib.cloneNode(true));
|
|
const rm = sib; sib = sib.nextElementSibling; rm.remove();
|
|
}
|
|
if (sib && sib.tagName === 'HR') sib.remove();
|
|
block.appendChild(avatarCol); block.appendChild(contentCol);
|
|
p.parentNode.insertBefore(block, p); p.remove();
|
|
});
|
|
// Highlight anchored post (works with any ID including remarkbox IDs)
|
|
if (location.hash) {
|
|
const target = document.querySelector(location.hash);
|
|
if (target && (target.classList.contains('post-block') || target.closest('.post-block'))) {
|
|
const post = target.classList.contains('post-block') ? target : target.closest('.post-block');
|
|
post.scrollIntoView({behavior: 'smooth', block: 'center'});
|
|
post.classList.add('post-highlighted');
|
|
setTimeout(() => post.classList.remove('post-highlighted'), 8000);
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
{% if not noai %}
|
|
<script>
|
|
window.UNCLOSEAI_LANGUAGE = "{{ lang }}";
|
|
window.UNCLOSEAI_SYSTEM_PROMPT = "You are a neopig assistant viewing an archived page from {{ source_domain }}. Page title: {{ title }}. neopig archived this - not the original content creator. Help users understand this archived page factually.";
|
|
</script>
|
|
<script src="https://uncloseai.com/uncloseai.js?v=3" type="module"></script>
|
|
{% endif %}
|
|
{% endblock %}
|