utf9k A robot icon

Hey, I’m Marcus. I’m a Software Developer based in Auckland, New Zealand.

Welcome to my small patch of soil on the internet.

Latest Activity
Loading...
Current Media Diet
Box art for Paranormasight: The Mermaid's Curse
Box art for Paranormasight: The Mermaid's Curse
Paranormasight: The Mermaid's Curse
Book cover for Impro
Book cover for Impro
Impro
Keith Johnstone
5% read
Manga
Book cover for Chainsaw Man
Book cover for Chainsaw Man
Chainsaw Man
Tatsuki Fujimoto
230 chapters read · 232 chapters total
Book cover for Final Fantasy XIV: Shiritsu Eorzea Gakuen
Book cover for Final Fantasy XIV: Shiritsu Eorzea Gakuen
Final Fantasy XIV: Shiritsu Eorzea Gakuen
Esora Amaichi
3 chapters read · 7 chapters total
Book cover for Kingdom Hearts: 358/2 Days
Book cover for Kingdom Hearts: 358/2 Days
Kingdom Hearts: 358/2 Days
Shiro Amano
1 chapter read · 35 chapters total
Book cover for Shuma Baobei 03: Xun Shou Shi Zhi Wang
Book cover for Shuma Baobei 03: Xun Shou Shi Zhi Wang
Shuma Baobei 03: Xun Shou Shi Zhi Wang
Stephanie Sheh
1 chapter read · 30 chapters total
${item.subtitle || ''}
`; return preview; } function setupPreviewHandlers(card, preview) { card.addEventListener('click', e => { e.stopPropagation(); const wasActive = preview.classList.contains('active'); document.querySelectorAll('.preview.active').forEach(p => p.classList.remove('active', 'clicked')); if (!wasActive) { preview.classList.add('active', 'clicked'); } }); card.addEventListener('mouseenter', e => { card.classList.add('hovered'); if (e.shiftKey && !preview.classList.contains('clicked')) { document.querySelectorAll('.preview.active').forEach(p => { if (!p.classList.contains('clicked')) p.classList.remove('active'); }); preview.classList.add('active'); } }); card.addEventListener('mouseleave', e => { card.classList.remove('hovered'); if (!e.shiftKey && !preview.classList.contains('clicked')) { preview.classList.remove('active'); } }); preview.addEventListener('click', e => { e.stopPropagation(); preview.classList.remove('active', 'clicked'); }); } let cachedPlaying = null; let cachedHistory = null; let lastItemCount = 0; let activityLoaded = false; const CARD_WIDTH = 120; const GAP = 9.6; const LOAD_TIMEOUT = 10000; function getItemCount() { const container = document.getElementById('activity-grid'); const containerWidth = container.offsetWidth; return Math.floor((containerWidth + GAP) / (CARD_WIDTH + GAP)); } function renderError() { const container = document.getElementById('activity-grid'); container.innerHTML = '
It looks like my activity backend server is having trouble! You\'re missing out on a cool feature sadly.
'; } function renderActivity(playing, history) { if (playing !== undefined) cachedPlaying = playing; if (history !== undefined) cachedHistory = history; if (!cachedHistory) return; activityLoaded = true; const container = document.getElementById('activity-grid'); container.innerHTML = ''; const activePlaying = cachedPlaying ? cachedPlaying.filter(p => p.is_active) : []; const activeIds = new Set(activePlaying.map(p => p.id)); const filteredHistory = cachedHistory.filter(h => !activeIds.has(h.id)); const maxItems = getItemCount(); lastItemCount = maxItems; const items = [...activePlaying, ...filteredHistory].slice(0, maxItems); items.forEach((item, index) => { const isNowPlaying = activeIds.has(item.id) && item.is_active; let title = item.category === MANGA ? formatMangaTitle(item.title) : item.title; const card = document.createElement('div'); card.className = `activity-card cat-${item.category}${isNowPlaying ? ' now-playing' : ''}`; const imgUrl = item.image.startsWith('http') ? item.image : "https://gunslinger.utf9k.net" + item.image; const img = document.createElement('img'); img.src = imgUrl; img.alt = title; card.appendChild(img); const preview = createPreview(item, title, imgUrl); card.appendChild(preview); setupPreviewHandlers(card, preview); const time = isNowPlaying ? 'Now playing' : relativeTime(item); const categoryLabel = categoryLabels[item.category] || item.category; const meta = document.createElement('div'); meta.className = 'activity-meta'; meta.innerHTML = `
${categoryLabel}
${item.subtitle || ''}
${title}
${time ? `
${time}
` : ''} `; card.appendChild(meta); container.appendChild(card); }); } function fetchData() { Promise.all([ fetch("https://gunslinger.utf9k.net/api/v4/playing").then(res => res.json()), fetch("https://gunslinger.utf9k.net/api/v4/history").then(res => res.json()) ]) .then(([playing, history]) => { updateSun(playing); renderActivity(playing, history); }) .catch(err => console.error(`Failed to fetch activity: ${err}`)); } function initEventSource() { return new EventSource("https://gunslinger.utf9k.net/events?stream=playback"); } let eventSource = initEventSource(); eventSource.onmessage = function(event) { let data = JSON.parse(event.data); if (data === null) { fetchData(); return; } if (data.length > 0 && data.every(item => item.started_at < 0)) return; fetchData(); }; document.addEventListener("visibilitychange", () => { if (document.hidden) { eventSource.close(); } else { fetchData(); eventSource = initEventSource(); } }); document.addEventListener('keydown', e => { if (e.key === 'Shift') { document.querySelectorAll('.activity-card.hovered').forEach(card => { const preview = card.querySelector('.preview'); if (preview && !preview.classList.contains('clicked')) { preview.classList.add('active'); } }); } if (e.key === 'Escape') { document.querySelectorAll('.activity-card .preview.active').forEach(p => p.classList.remove('active', 'clicked')); } }); document.addEventListener('keyup', e => { if (e.key === 'Shift') { document.querySelectorAll('.activity-card .preview.active').forEach(p => { if (!p.classList.contains('clicked')) p.classList.remove('active'); }); } }); document.addEventListener('click', e => { if (!e.target.closest('.activity-card')) { document.querySelectorAll('.activity-card .preview.active').forEach(p => p.classList.remove('active', 'clicked')); } }); let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (getItemCount() !== lastItemCount) renderActivity(); }, 100); }); fetchData(); setTimeout(() => { if (!activityLoaded) renderError(); }, LOAD_TIMEOUT); })();