pig.py/templates/view.html.j2
Russell Ballestrini f124828c16 21 side quests scattered through the codebase
1-20 are findable. 21 is the absence itself.

fnord
2026-01-05 21:10:02 -05:00

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 %}