2025-08-03 21:53:15 -04:00
|
|
|
<script>
|
|
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
import { browser } from '$app/environment';
|
2026-01-05 22:54:27 -05:00
|
|
|
import { audioPlaylist, currentTrack } from '$lib/stores/audioPlaylist';
|
|
|
|
|
import { siteSettings } from '$lib/stores/siteSettings';
|
|
|
|
|
import { connectWebSocket, disconnectWebSocket } from '$lib/websocket.js';
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
let streams = [];
|
2026-01-05 22:54:27 -05:00
|
|
|
let videos = [];
|
|
|
|
|
let audioFiles = [];
|
|
|
|
|
let ebookFiles = [];
|
|
|
|
|
let watchRooms = [];
|
2025-08-03 21:53:15 -04:00
|
|
|
let interval;
|
|
|
|
|
let loading = true;
|
2026-01-05 22:54:27 -05:00
|
|
|
let videosLoading = true;
|
|
|
|
|
let audioLoading = true;
|
|
|
|
|
let ebooksLoading = true;
|
|
|
|
|
let watchRoomsLoading = true;
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
async function loadStreams() {
|
|
|
|
|
if (!browser) return;
|
2026-01-05 22:54:27 -05:00
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/realms/live');
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
streams = await res.json();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load streams:', e);
|
|
|
|
|
} finally {
|
|
|
|
|
loading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
async function loadVideos() {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/videos/latest');
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
videos = data.videos || [];
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load videos:', e);
|
|
|
|
|
} finally {
|
|
|
|
|
videosLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadAudio() {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/audio/latest');
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
audioFiles = data.audio || [];
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load audio:', e);
|
|
|
|
|
} finally {
|
|
|
|
|
audioLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadEbooks() {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/ebooks/latest');
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
ebookFiles = data.ebooks || [];
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load ebooks:', e);
|
|
|
|
|
} finally {
|
|
|
|
|
ebooksLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadWatchRooms() {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/watch/rooms');
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
watchRooms = data.rooms || [];
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to load watch rooms:', e);
|
|
|
|
|
} finally {
|
|
|
|
|
watchRoomsLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDuration(seconds) {
|
|
|
|
|
if (!seconds) return '0:00';
|
|
|
|
|
const mins = Math.floor(seconds / 60);
|
|
|
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatViews(count) {
|
|
|
|
|
if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M';
|
|
|
|
|
if (count >= 1000) return (count / 1000).toFixed(1) + 'K';
|
|
|
|
|
return count.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatBitrate(bps) {
|
|
|
|
|
if (!bps) return '';
|
|
|
|
|
const kbps = Math.round(bps / 1000);
|
|
|
|
|
if (kbps >= 1000) return (kbps / 1000).toFixed(1) + ' Mbps';
|
|
|
|
|
return kbps + ' kbps';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function togglePlaylist(audio) {
|
|
|
|
|
// If already in playlist, remove it; otherwise add it
|
|
|
|
|
if (playlistIds.has(audio.id)) {
|
|
|
|
|
audioPlaylist.removeTrack(audio.id);
|
|
|
|
|
} else {
|
|
|
|
|
audioPlaylist.addTrack({
|
|
|
|
|
id: audio.id,
|
|
|
|
|
title: audio.title,
|
|
|
|
|
username: audio.username,
|
|
|
|
|
filePath: audio.filePath,
|
|
|
|
|
thumbnailPath: audio.thumbnailPath || '',
|
|
|
|
|
durationSeconds: audio.durationSeconds,
|
|
|
|
|
realmName: audio.realmName
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function togglePlay(audio) {
|
|
|
|
|
// If this audio is currently playing, toggle play/pause
|
|
|
|
|
if ($currentTrack && $currentTrack.id === audio.id) {
|
|
|
|
|
audioPlaylist.togglePlay();
|
|
|
|
|
} else {
|
|
|
|
|
// Otherwise, play this track
|
|
|
|
|
audioPlaylist.playTrack({
|
|
|
|
|
id: audio.id,
|
|
|
|
|
title: audio.title,
|
|
|
|
|
username: audio.username,
|
|
|
|
|
filePath: audio.filePath,
|
|
|
|
|
thumbnailPath: audio.thumbnailPath || '',
|
|
|
|
|
durationSeconds: audio.durationSeconds,
|
|
|
|
|
realmName: audio.realmName
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a specific audio is currently playing
|
|
|
|
|
function isCurrentlyPlaying(audioId) {
|
|
|
|
|
return $currentTrack && $currentTrack.id === audioId && $audioPlaylist.isPlaying;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reactive set of playlist IDs for checking if audio is in playlist
|
|
|
|
|
$: playlistIds = new Set($audioPlaylist.queue.map(t => t.id));
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
onMount(() => {
|
|
|
|
|
loadStreams();
|
2026-01-05 22:54:27 -05:00
|
|
|
loadVideos();
|
|
|
|
|
loadAudio();
|
|
|
|
|
loadEbooks();
|
|
|
|
|
loadWatchRooms();
|
|
|
|
|
// Refresh streams every 10 seconds
|
2025-08-03 21:53:15 -04:00
|
|
|
interval = setInterval(loadStreams, 10000);
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
// Listen for stream status changes via WebSocket for instant refresh
|
|
|
|
|
connectWebSocket((data) => {
|
|
|
|
|
if (data.type === 'stream_live' || data.type === 'stream_offline') {
|
|
|
|
|
loadStreams(); // Immediate refresh when stream status changes
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Dev console easter egg
|
|
|
|
|
console.log(`%c
|
|
|
|
|
⠀⠀⠀⣤⣄⡀⠀⠀⠀⠀∣⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
|
|
|
⠀⠀⢰⠈⣿⣿⣿⣦⡄⠀⠀⠀⠹⠿⠿⠿⢿⣿⣿⠟⠋⢁⢀⣿
|
|
|
|
|
⠀⠀⠘⡆⠙⣿⣿⡄⠀⠀⠀⠈⢙⣀⡀⠀⣀⠀⠀⠠⠶⣡⣿⣿
|
|
|
|
|
⣷⡄⠀⠙⣐⣬⠁⠀⠀⠐⡎⣊⣉⡍⠃⢸⣿⣷⢒⡒⢆⠙⣿⣿
|
|
|
|
|
⠆⣁⠠⠀⣀⣒⠒⠂⢄⣐⣧⣉⣀⡴⢃⡼⢿⣧⣐⣠⡾⢠⣿⣿
|
|
|
|
|
⡥⠒⠿⠧⢄⠀⠘⠛⠛⠛⣓⣦⣈⢤⣿⣿⣦⡍⣡⡧⡄⠀⣛⣻
|
|
|
|
|
⡒⠶⠖⠒⠛⠋⠉⣋⣭⣿⣉⣀⠈⠳⠿⠟⠋⠘⣿⣯⠗⠠⢤⣐
|
|
|
|
|
⢑⠊⠂⢀⠠⠒⢪⢉⣰⣦⣑⠋⣀⡀⢀⣀⠀⠀⠘⠀⣤⣛⠶⣾
|
|
|
|
|
⠀⠠⠙⠚⡐⣼⣾⣿⣿⠿⣿⣆⠹⣿⣾⡏⣤⡇⣶⣿⣾⣿⣿⣿
|
|
|
|
|
⠀⠢⢻⣀⠀⠙⡛⡛⠃⠀⢻⣮⠑⠈⣉⣠⠞⣴⣿⣿⣿⣿⣿⣿
|
|
|
|
|
⠀⠒⠁⣐⠤⣱⢄⡒⡀⢀⠈⠛⠿⠒⠂⠀⣾⣿⣿⣿⣿⣿⣿⣿
|
|
|
|
|
⠀⠂⠀⠀⡑⠈⠻⠻⠀⡀⠀⡀⠋⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿
|
|
|
|
|
`, 'color: #561D5E; font-size: 10px; font-family: monospace;');
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
return () => {
|
|
|
|
|
if (interval) clearInterval(interval);
|
2026-01-05 22:54:27 -05:00
|
|
|
disconnectWebSocket();
|
2025-08-03 21:53:15 -04:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-01-05 22:54:27 -05:00
|
|
|
<svelte:head>
|
|
|
|
|
<title>{$siteSettings.site_title}</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
<style>
|
|
|
|
|
.hero {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 4rem 0;
|
|
|
|
|
margin-bottom: 3rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero h1 {
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
background: linear-gradient(135deg, var(--primary), #8b3a92);
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero p {
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
|
|
gap: 2rem;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-card {
|
|
|
|
|
background: #111;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
color: var(--white);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-card:hover {
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
box-shadow: 0 8px 24px rgba(86, 29, 94, 0.3);
|
|
|
|
|
border-color: var(--primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-thumbnail {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 180px;
|
|
|
|
|
background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
position: relative;
|
2026-01-05 22:54:27 -05:00
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-thumbnail img {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-thumbnail .placeholder-container {
|
2026-01-08 19:42:22 -05:00
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
2026-01-05 22:54:27 -05:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
z-index: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 19:42:22 -05:00
|
|
|
.stream-thumbnail .offline-placeholder-img {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
z-index: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 22:54:27 -05:00
|
|
|
.stream-thumbnail .placeholder-initial {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: linear-gradient(135deg, var(--primary), #8b3a92);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 1.75rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: white;
|
|
|
|
|
position: relative;
|
2026-01-08 19:42:22 -05:00
|
|
|
z-index: 1;
|
2026-01-05 22:54:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-thumbnail .live-pulse {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: -4px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border: 2px solid var(--primary);
|
|
|
|
|
animation: pulse 2s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
0%, 100% {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
}
|
|
|
|
|
50% {
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
2025-08-03 21:53:15 -04:00
|
|
|
}
|
2026-01-05 22:54:27 -05:00
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
|
|
|
|
|
.live-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 1rem;
|
|
|
|
|
left: 1rem;
|
|
|
|
|
background: #ff0000;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 0.25rem 0.75rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
2026-01-05 22:54:27 -05:00
|
|
|
z-index: 2;
|
2025-08-03 21:53:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-info {
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-info h3 {
|
|
|
|
|
margin: 0 0 0.5rem 0;
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stream-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.streamer-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.streamer-avatar {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.viewer-count {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.viewer-count::before {
|
|
|
|
|
content: '•';
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
background: #ff0000;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
margin-right: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-streams {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 4rem 0;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-streams-icon {
|
|
|
|
|
font-size: 4rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
/* Videos section */
|
|
|
|
|
.videos-section {
|
|
|
|
|
margin-top: 4rem;
|
|
|
|
|
padding-top: 2rem;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-header h2 {
|
|
|
|
|
font-size: 1.75rem;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.see-all-link {
|
|
|
|
|
color: var(--primary);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.see-all-link:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-card {
|
|
|
|
|
background: #111;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
color: var(--white);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-card:hover {
|
|
|
|
|
transform: translateY(-3px);
|
|
|
|
|
box-shadow: 0 6px 20px rgba(86, 29, 94, 0.25);
|
|
|
|
|
border-color: var(--primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-thumbnail {
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 16/9;
|
|
|
|
|
background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-thumbnail img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-thumbnail .placeholder {
|
|
|
|
|
font-size: 2.5rem;
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.duration-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 0.5rem;
|
|
|
|
|
right: 0.5rem;
|
|
|
|
|
background: rgba(0, 0, 0, 0.85);
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 0.15rem 0.4rem;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-info {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-info h3 {
|
|
|
|
|
margin: 0 0 0.5rem 0;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-uploader {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.video-uploader-avatar {
|
|
|
|
|
width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Audio section - Line item style */
|
|
|
|
|
.audio-section {
|
|
|
|
|
margin-top: 4rem;
|
|
|
|
|
padding-top: 2rem;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
background: #111;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-item:hover {
|
|
|
|
|
border-color: #ec4899;
|
|
|
|
|
background: rgba(236, 72, 153, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-number {
|
|
|
|
|
width: 24px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-thumb {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-thumb img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-thumb .placeholder {
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-title {
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--white);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
margin-top: 0.15rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-realm-link {
|
|
|
|
|
color: #ec4899;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-realm-link:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-bitrate {
|
|
|
|
|
color: #ec4899;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-duration {
|
2026-01-09 18:37:34 -05:00
|
|
|
font-family: var(--font-mono);
|
2026-01-05 22:54:27 -05:00
|
|
|
color: var(--gray);
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
min-width: 45px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-action-btn {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-action-btn:hover {
|
|
|
|
|
background: rgba(236, 72, 153, 0.2);
|
|
|
|
|
border-color: #ec4899;
|
|
|
|
|
color: #ec4899;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-action-btn.play {
|
|
|
|
|
background: rgba(236, 72, 153, 0.2);
|
|
|
|
|
border-color: rgba(236, 72, 153, 0.4);
|
|
|
|
|
color: #ec4899;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-action-btn.play:hover {
|
|
|
|
|
background: #ec4899;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-action-btn.added {
|
|
|
|
|
background: rgba(126, 231, 135, 0.2);
|
|
|
|
|
border-color: #7ee787;
|
|
|
|
|
color: #7ee787;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Watch rooms section */
|
|
|
|
|
.watch-rooms-section {
|
|
|
|
|
margin-top: 3rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-rooms-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-card {
|
|
|
|
|
background: #111;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
color: var(--white);
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-card:hover {
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
box-shadow: 0 8px 24px rgba(86, 29, 94, 0.3);
|
|
|
|
|
border-color: var(--primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-thumbnail {
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 16/9;
|
|
|
|
|
background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-thumbnail img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-placeholder {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-live-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 8px;
|
|
|
|
|
left: 8px;
|
|
|
|
|
background: var(--primary);
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-queue-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 8px;
|
|
|
|
|
right: 8px;
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-info {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-info h3 {
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
margin: 0 0 0.5rem 0;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-current {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-owner {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-avatar {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-avatar img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-room-viewers {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Ebooks section */
|
|
|
|
|
.ebooks-section {
|
|
|
|
|
margin-top: 3rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebooks-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-card {
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
color: var(--white);
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-card:hover {
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-cover {
|
|
|
|
|
aspect-ratio: 2/3;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: #1a1a1a;
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-cover img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-cover-placeholder {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-cover-placeholder span {
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-info h3 {
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
margin: 0 0 0.25rem 0;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-author {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
display: block;
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-meta {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: #666;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ebook-uploader {
|
|
|
|
|
color: #10b981;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Watch Realms Section */
|
|
|
|
|
.watch-realms-section {
|
|
|
|
|
margin-top: 3rem;
|
|
|
|
|
padding-top: 2rem;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realms-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realms-list.offline {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realms-offline-label {
|
|
|
|
|
margin-top: 1.5rem;
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-item {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 12px 1fr 2fr auto;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
background: #111;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
color: var(--white);
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-item:hover {
|
|
|
|
|
background: #1a1a1a;
|
|
|
|
|
border-color: var(--primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-item.offline:hover {
|
|
|
|
|
border-color: var(--border);
|
|
|
|
|
background: #151515;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-status {
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--gray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-status.playing {
|
|
|
|
|
background: #22c55e;
|
|
|
|
|
box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-name {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-video {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
color: var(--gray);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-video.empty {
|
|
|
|
|
font-style: italic;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-owner {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.watch-realm-item {
|
|
|
|
|
grid-template-columns: 12px 1fr;
|
|
|
|
|
grid-template-rows: auto auto;
|
|
|
|
|
gap: 0.25rem 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-video {
|
|
|
|
|
grid-column: 2;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.watch-realm-owner {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-03 21:53:15 -04:00
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="hero">
|
|
|
|
|
<h1>Live Streams</h1>
|
|
|
|
|
<p>Watch your favorite streamers live</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if loading}
|
|
|
|
|
<div style="text-align: center; padding: 2rem;">
|
|
|
|
|
<p>Loading streams...</p>
|
|
|
|
|
</div>
|
|
|
|
|
{:else if streams.length === 0}
|
|
|
|
|
<div class="no-streams">
|
|
|
|
|
<div class="no-streams-icon">📺</div>
|
|
|
|
|
<h2>No streams live right now</h2>
|
|
|
|
|
<p>Check back later or become a streamer yourself!</p>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="stream-grid">
|
|
|
|
|
{#each streams as stream}
|
|
|
|
|
<a href={`/${stream.name}/live`} class="stream-card">
|
|
|
|
|
<div class="stream-thumbnail">
|
|
|
|
|
<div class="live-badge">LIVE</div>
|
2026-01-08 19:42:22 -05:00
|
|
|
<img
|
|
|
|
|
src={`/thumb/${encodeURIComponent(stream.name)}.webp`}
|
|
|
|
|
alt={stream.name}
|
|
|
|
|
on:error={(e) => e.target.style.display = 'none'}
|
|
|
|
|
/>
|
2026-01-05 22:54:27 -05:00
|
|
|
<div class="placeholder-container">
|
2026-01-08 19:42:22 -05:00
|
|
|
{#if stream.offlineImageUrl}
|
|
|
|
|
<img src={stream.offlineImageUrl} alt="" class="offline-placeholder-img" />
|
|
|
|
|
{/if}
|
2026-01-05 22:54:27 -05:00
|
|
|
<div class="placeholder-initial">
|
|
|
|
|
<div class="live-pulse"></div>
|
|
|
|
|
{stream.name.charAt(0).toUpperCase()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-08-03 21:53:15 -04:00
|
|
|
</div>
|
|
|
|
|
<div class="stream-info">
|
2026-01-05 22:54:27 -05:00
|
|
|
<h3 style="color: {stream.titleColor || '#ffffff'};">{stream.name}</h3>
|
2025-08-03 21:53:15 -04:00
|
|
|
<div class="stream-meta">
|
|
|
|
|
<div class="streamer-info">
|
|
|
|
|
{#if stream.avatarUrl}
|
|
|
|
|
<img src={stream.avatarUrl} alt={stream.username} class="streamer-avatar" />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="streamer-avatar"></div>
|
|
|
|
|
{/if}
|
2026-01-09 01:56:05 -05:00
|
|
|
<span style="color: {stream.colorCode}">{stream.username}</span>
|
2025-08-03 21:53:15 -04:00
|
|
|
</div>
|
|
|
|
|
<div class="viewer-count">
|
|
|
|
|
{stream.viewerCount} {stream.viewerCount === 1 ? 'viewer' : 'viewers'}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
<!-- Latest Videos Section -->
|
|
|
|
|
{#if !videosLoading && videos.length > 0}
|
|
|
|
|
<div class="videos-section">
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
<h2>Latest Videos</h2>
|
|
|
|
|
<a href="/video" class="see-all-link">See all videos</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="video-grid">
|
|
|
|
|
{#each videos as video}
|
|
|
|
|
<a href={`/watch/${video.id}`} class="video-card">
|
|
|
|
|
<div class="video-thumbnail">
|
|
|
|
|
{#if video.thumbnailPath}
|
|
|
|
|
<img src={video.thumbnailPath} alt={video.title} />
|
|
|
|
|
{:else}
|
|
|
|
|
<span class="placeholder">🎬</span>
|
|
|
|
|
{/if}
|
|
|
|
|
<span class="duration-badge">{formatDuration(video.durationSeconds)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="video-info">
|
|
|
|
|
<h3>{video.title}</h3>
|
|
|
|
|
<div class="video-meta">
|
|
|
|
|
<div class="video-uploader">
|
|
|
|
|
{#if video.avatarUrl}
|
|
|
|
|
<img src={video.avatarUrl} alt={video.username} class="video-uploader-avatar" />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="video-uploader-avatar"></div>
|
|
|
|
|
{/if}
|
|
|
|
|
<span>{video.username}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{formatViews(video.viewCount)} views</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Latest Audio Section -->
|
|
|
|
|
{#if !audioLoading && audioFiles.length > 0}
|
|
|
|
|
<div class="audio-section">
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
<h2>Latest Audio</h2>
|
|
|
|
|
<a href="/audio" class="see-all-link">See all audio</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="audio-list">
|
|
|
|
|
{#each audioFiles.slice(0, 5) as audio, index}
|
|
|
|
|
<div class="audio-item">
|
|
|
|
|
<span class="audio-number">{index + 1}</span>
|
|
|
|
|
<div class="audio-thumb">
|
|
|
|
|
{#if audio.thumbnailPath}
|
|
|
|
|
<img src={audio.thumbnailPath} alt={audio.title} />
|
|
|
|
|
{:else}
|
|
|
|
|
<span class="placeholder">🎵</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="audio-info">
|
|
|
|
|
<div class="audio-title">{audio.title}</div>
|
|
|
|
|
<div class="audio-meta">
|
|
|
|
|
<span>{audio.username}</span>
|
|
|
|
|
{#if audio.realmName}
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<a href={`/audio/${encodeURIComponent(audio.realmName)}`} class="audio-realm-link">{audio.realmName}</a>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if audio.bitrate}
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span class="audio-bitrate">{formatBitrate(audio.bitrate)}</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="audio-duration">{formatDuration(audio.durationSeconds)}</span>
|
|
|
|
|
<div class="audio-actions">
|
|
|
|
|
<button
|
|
|
|
|
class="audio-action-btn play"
|
|
|
|
|
class:playing={isCurrentlyPlaying(audio.id)}
|
|
|
|
|
on:click={() => togglePlay(audio)}
|
|
|
|
|
title={isCurrentlyPlaying(audio.id) ? 'Pause' : 'Play'}
|
|
|
|
|
>
|
|
|
|
|
{isCurrentlyPlaying(audio.id) ? '❚❚' : '▶'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="audio-action-btn"
|
|
|
|
|
class:added={playlistIds.has(audio.id)}
|
|
|
|
|
on:click={() => togglePlaylist(audio)}
|
|
|
|
|
title={playlistIds.has(audio.id) ? 'Remove from playlist' : 'Add to playlist'}
|
|
|
|
|
>
|
|
|
|
|
{playlistIds.has(audio.id) ? '✓' : '+'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Watch Rooms Section - Only show rooms with a video currently playing -->
|
|
|
|
|
{#if !watchRoomsLoading && watchRooms.filter(r => r.currentVideoTitle).length > 0}
|
|
|
|
|
<div class="watch-rooms-section">
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
<h2>Watch Together</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="watch-rooms-grid">
|
|
|
|
|
{#each watchRooms.filter(r => r.currentVideoTitle) as room}
|
|
|
|
|
<a href={`/${room.name}/watch`} class="watch-room-card">
|
|
|
|
|
<div class="watch-room-thumbnail">
|
|
|
|
|
{#if room.currentThumbnail}
|
|
|
|
|
<img src={room.currentThumbnail} alt={room.currentVideoTitle || 'Now playing'} />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="watch-room-placeholder">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="currentColor" width="48" height="48">
|
|
|
|
|
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if room.playbackState === 'playing'}
|
|
|
|
|
<span class="watch-room-live-badge">PLAYING</span>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if room.queuedCount > 0}
|
|
|
|
|
<span class="watch-room-queue-badge">{room.queuedCount} in queue</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="watch-room-info">
|
|
|
|
|
<h3>{room.name}</h3>
|
|
|
|
|
{#if room.currentVideoTitle}
|
|
|
|
|
<div class="watch-room-current" title={room.currentVideoTitle}>
|
|
|
|
|
{room.currentVideoTitle}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="watch-room-meta">
|
|
|
|
|
<div class="watch-room-owner">
|
|
|
|
|
{#if room.avatarUrl}
|
|
|
|
|
<img src={room.avatarUrl} alt={room.username} class="watch-room-avatar" />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="watch-room-avatar" style="background: {room.colorCode}">
|
|
|
|
|
{room.username?.charAt(0).toUpperCase() || '?'}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
<span style="color: {room.colorCode}">{room.username}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{#if room.viewerCount > 0}
|
|
|
|
|
<span class="watch-room-viewers">{room.viewerCount} watching</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Latest Ebooks Section -->
|
|
|
|
|
{#if !ebooksLoading && ebookFiles.length > 0}
|
|
|
|
|
<div class="ebooks-section">
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
<h2>Latest Ebooks</h2>
|
|
|
|
|
<a href="/ebooks" class="see-all-link">See all ebooks</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ebooks-grid">
|
|
|
|
|
{#each ebookFiles as ebook}
|
|
|
|
|
<a href={`/read/${ebook.id}`} class="ebook-card">
|
|
|
|
|
<div class="ebook-cover">
|
|
|
|
|
{#if ebook.coverPath}
|
|
|
|
|
<img src={ebook.coverPath} alt={ebook.title} />
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="ebook-cover-placeholder">
|
|
|
|
|
<span>📚</span>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ebook-info">
|
|
|
|
|
<h3>{ebook.title}</h3>
|
|
|
|
|
{#if ebook.author}
|
|
|
|
|
<span class="ebook-author">{ebook.author}</span>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="ebook-meta">
|
|
|
|
|
<span class="ebook-uploader">by @{ebook.username}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
</div>
|