aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-05-14 05:10:28 +0530
committerBobby <[email protected]>2026-05-14 05:10:28 +0530
commite229c334e9491bd7dd6399a1716d641dcb6ee2ad (patch)
treee66b1e8f1fd6b03a547efcee1a2ae5549fcc3d6d
parent6cf63b8d08c7b1d8cb144efb3378b249b7b21629 (diff)
downloadthatcomputerscientist-e229c334e9491bd7dd6399a1716d641dcb6ee2ad.tar.xz
thatcomputerscientist-e229c334e9491bd7dd6399a1716d641dcb6ee2ad.zip
Add screenshots page with Steam integration on CRT GIF
-rw-r--r--core/urls.py1
-rw-r--r--core/views.py9
-rw-r--r--internal/steam_wrapper.py91
-rw-r--r--static/css/core/screenshots.css76
-rw-r--r--static/images/core/backgrounds/screenshot.gifbin0 -> 1091859 bytes
-rw-r--r--templates/_partials/left_sidebar.html2
-rw-r--r--templates/core/screenshots.html88
7 files changed, 266 insertions, 1 deletions
diff --git a/core/urls.py b/core/urls.py
index 4d5fa85e..598bd806 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -4,6 +4,7 @@ from . import views
app_name = "core"
urlpatterns = [
path("", views.home, name="home"),
+ path("screenshots", views.screenshots, name="screenshots"),
path("journal", views.journal, name="journal_default"),
path("journal/<slug:slug>", views.journal, name="journal"),
path("letters", include("core.letters.urls")),
diff --git a/core/views.py b/core/views.py
index e46e9773..86608b5f 100644
--- a/core/views.py
+++ b/core/views.py
@@ -14,6 +14,7 @@ from administration.annoucements.functions import get_announcements
from authentication.functions import get_user_from_username
from blog.functions import get_posts
from internal.mal_wrapper import get_mal_recent_activity
+from internal.steam_wrapper import get_steam_screenshots
from services.journals.functions import (
get_latest_journal_entry,
get_journal,
@@ -56,6 +57,14 @@ def home(request: HttpRequest) -> HttpResponse:
return render(request, "core/home.html", context)
+def screenshots(request: HttpRequest) -> HttpResponse:
+ title_map = {"en": "Screenshots", "ja": "スクリーンショット"}
+ request.meta.title = title_map.get(request.LANGUAGE_CODE)
+
+ shots = get_steam_screenshots(os.getenv("STEAM_USERNAME", ""))
+ return render(request, "core/screenshots.html", {"shots": shots})
+
+
def journal(request: HttpRequest, slug: str = "journal-of-random-thoughts") -> HttpResponse:
page = int(request.GET.get("page", 1))
entry_slug = request.GET.get("entry")
diff --git a/internal/steam_wrapper.py b/internal/steam_wrapper.py
new file mode 100644
index 00000000..67f7e0e6
--- /dev/null
+++ b/internal/steam_wrapper.py
@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+import os
+
+import requests
+
+from internal.cache import cache
+
+
+STEAM_API_KEY = os.getenv("STEAM_API_KEY", "")
+SCREENSHOT_TTL = 6 * 60 * 60
+VANITY_TTL = 30 * 24 * 60 * 60
+
+
+def _resolve_vanity(username: str) -> str | None:
+ if not username:
+ return None
+ if username.isdigit():
+ return username
+ cache_key = f"steam_vanity:{username}"
+ cached = cache.get(cache_key)
+ if cached:
+ return cached
+ if not STEAM_API_KEY:
+ return None
+ url = "https://api.steampowered.com/ISteamUser/ResolveVanityURL/v1/"
+ try:
+ r = requests.get(url, params={"key": STEAM_API_KEY, "vanityurl": username}, timeout=10)
+ payload = r.json().get("response", {})
+ except (requests.RequestException, ValueError):
+ return None
+ if payload.get("success") != 1:
+ return None
+ sid = str(payload.get("steamid", ""))
+ if sid:
+ cache.set(cache_key, sid, ex=VANITY_TTL)
+ return sid or None
+
+
+def get_steam_screenshots(username: str) -> list[dict]:
+ """Return every screenshot as a flat list in Steam's native order (newest
+ first across all games — no per-game grouping)."""
+ if not username:
+ return []
+ cache_key = f"steam_screenshots_flat:{username}"
+ cached = cache.get(cache_key)
+ if cached is not None:
+ return cached
+
+ steam_id = _resolve_vanity(username)
+ if not steam_id or not STEAM_API_KEY:
+ return []
+
+ shots: list[dict] = []
+ page = 1
+ per_page = 100
+ while page <= 4:
+ params = {
+ "key": STEAM_API_KEY,
+ "steamid": steam_id,
+ "page": page,
+ "numperpage": per_page,
+ "filetype": 4,
+ "return_metadata": 1,
+ }
+ try:
+ r = requests.get(
+ "https://api.steampowered.com/IPublishedFileService/GetUserFiles/v1/",
+ params=params,
+ timeout=15,
+ )
+ data = r.json().get("response", {})
+ except (requests.RequestException, ValueError):
+ break
+ files = data.get("publishedfiledetails", []) or []
+ if not files:
+ break
+ for f in files:
+ full = f.get("file_url") or f.get("preview_url") or ""
+ if not full:
+ continue
+ shots.append({
+ "id": str(f.get("publishedfileid", "")),
+ "url": full,
+ })
+ if len(files) < per_page:
+ break
+ page += 1
+
+ cache.set(cache_key, shots, ex=SCREENSHOT_TTL)
+ return shots
diff --git a/static/css/core/screenshots.css b/static/css/core/screenshots.css
new file mode 100644
index 00000000..ffb53858
--- /dev/null
+++ b/static/css/core/screenshots.css
@@ -0,0 +1,76 @@
+.scr-room {
+ width: 780px;
+ height: 780px;
+ margin: 8px auto 0;
+ background: #0a0a0a url('/static/images/core/backgrounds/screenshot.gif') no-repeat center top;
+ background-size: 780px 780px;
+ position: relative;
+ image-rendering: pixelated;
+}
+
+.scr-monitor {
+ position: absolute;
+ left: 122px;
+ top: 161px;
+ width: 408px;
+ height: 317px;
+ overflow: hidden;
+ cursor: zoom-in;
+ background: #000;
+}
+
+.scr-img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ image-rendering: auto;
+}
+
+.scr-noise {
+ position: absolute;
+ inset: 0;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ color: #f0e6d2;
+ font-family: 'Mali', sans-serif;
+ font-style: italic;
+ letter-spacing: 4px;
+ text-transform: uppercase;
+ font-size: 16px;
+ background: #000;
+}
+
+.scr-hint {
+ width: 780px;
+ margin: 12px auto 24px;
+ text-align: center;
+ color: #f0e6d2;
+ font-family: 'Mali', sans-serif;
+ font-size: 13px;
+ letter-spacing: 2px;
+}
+
+.scr-zoom {
+ position: fixed;
+ inset: 0;
+ background: rgba(8, 4, 18, 0.95);
+ z-index: 1000;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ cursor: zoom-out;
+ backdrop-filter: blur(6px);
+}
+
+.scr-zoom.is-open {
+ display: flex;
+}
+
+.scr-zoom-img {
+ max-width: 94vw;
+ max-height: 92vh;
+ object-fit: contain;
+ box-shadow: 0 18px 60px rgba(0, 0, 0, 0.7);
+}
diff --git a/static/images/core/backgrounds/screenshot.gif b/static/images/core/backgrounds/screenshot.gif
new file mode 100644
index 00000000..53732055
--- /dev/null
+++ b/static/images/core/backgrounds/screenshot.gif
Binary files differ
diff --git a/templates/_partials/left_sidebar.html b/templates/_partials/left_sidebar.html
index bd0fd8a6..45ed898d 100644
--- a/templates/_partials/left_sidebar.html
+++ b/templates/_partials/left_sidebar.html
@@ -110,7 +110,7 @@
<div class="navigation-item">
<img src="{% static 'images/core/icons/screenshots.png' %}" width="20" height="20"
alt="Screenshots Icon" />
- <a href="#screenshots">{% translate "Screenshots" %}</a>
+ <a href="{% url 'core:screenshots' %}">{% translate "Screenshots" %}</a>
</div>
<div class="navigation-item">
<img src="{% static 'images/core/icons/chatrooms.png' %}" width="20" height="20"
diff --git a/templates/core/screenshots.html b/templates/core/screenshots.html
new file mode 100644
index 00000000..f48d888e
--- /dev/null
+++ b/templates/core/screenshots.html
@@ -0,0 +1,88 @@
+{% extends "_layouts/base.html" %}
+{% load static %}
+{% load i18n %}
+
+{% block head %}
+<link type="text/css" rel="stylesheet" href="{% static 'css/core/screenshots.css' %}" />
+{% endblock head %}
+
+{% block content %}
+<div class="scr-room">
+ <div class="scr-monitor" id="scr-monitor">
+ <img class="scr-img" id="scr-img" alt="" />
+ <div class="scr-noise" id="scr-noise">{% trans "no signal" %}</div>
+ </div>
+</div>
+<div class="scr-hint" id="scr-hint">{% trans "press ← or → to change picture" %}</div>
+
+<div class="scr-zoom" id="scr-zoom">
+ <img class="scr-zoom-img" id="scr-zoom-img" alt="" />
+</div>
+
+<script id="scr-data" type="application/json">[{% for s in shots %}{"id":"{{ s.id }}","url":"{{ s.url }}"}{% if not forloop.last %},{% endif %}{% endfor %}]</script>
+
+<script>
+(function() {
+ var shots = JSON.parse(document.getElementById('scr-data').textContent || '[]');
+ var img = document.getElementById('scr-img');
+ var noise = document.getElementById('scr-noise');
+ var hint = document.getElementById('scr-hint');
+ var zoom = document.getElementById('scr-zoom');
+ var zoomImg = document.getElementById('scr-zoom-img');
+ var monitor = document.getElementById('scr-monitor');
+
+ if (!shots.length) {
+ img.style.display = 'none';
+ noise.style.display = 'flex';
+ hint.style.display = 'none';
+ return;
+ }
+
+ var idx = 0;
+ var cache = {};
+ var AHEAD = 4;
+
+ function preload(i) {
+ if (i < 0 || i >= shots.length || cache[i]) return;
+ var im = new Image();
+ im.src = shots[i].url;
+ cache[i] = im;
+ }
+
+ function show(i) {
+ idx = (i + shots.length) % shots.length;
+ img.src = shots[idx].url;
+ for (var j = 1; j <= AHEAD; j++) preload(idx + j);
+ preload(idx - 1);
+ hint.textContent = (idx + 1) + ' / ' + shots.length + ' — {% trans "press ← or → to change picture" %}';
+ }
+
+ function navigate(dir) {
+ show(idx + dir);
+ if (zoom.classList.contains('is-open')) zoomImg.src = shots[idx].url;
+ }
+
+ function openZoom() {
+ zoomImg.src = shots[idx].url;
+ zoom.classList.add('is-open');
+ document.body.style.overflow = 'hidden';
+ }
+
+ function closeZoom() {
+ zoom.classList.remove('is-open');
+ document.body.style.overflow = '';
+ }
+
+ monitor.addEventListener('click', openZoom);
+ zoom.addEventListener('click', closeZoom);
+
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' && zoom.classList.contains('is-open')) { closeZoom(); return; }
+ if (e.key === 'ArrowLeft') { e.preventDefault(); navigate(-1); }
+ else if (e.key === 'ArrowRight') { e.preventDefault(); navigate(1); }
+ });
+
+ show(0);
+})();
+</script>
+{% endblock content %}