This commit is contained in:
parent
33624d3b02
commit
896a3b77d7
5 changed files with 293 additions and 85 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
import { fly, fade, slide } from 'svelte/transition';
|
import { fly, fade, slide } from 'svelte/transition';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { isAuthenticated } from '$lib/stores/auth';
|
import { isAuthenticated } from '$lib/stores/auth';
|
||||||
|
import { getHolidaysForMonth } from '$lib/data/holidays.js';
|
||||||
import { connectionStatus } from '$lib/chat/chatStore';
|
import { connectionStatus } from '$lib/chat/chatStore';
|
||||||
import TerminalTabBar from '$lib/components/terminal/TerminalTabBar.svelte';
|
import TerminalTabBar from '$lib/components/terminal/TerminalTabBar.svelte';
|
||||||
import TerminalCore from '$lib/components/terminal/TerminalCore.svelte';
|
import TerminalCore from '$lib/components/terminal/TerminalCore.svelte';
|
||||||
|
|
@ -231,8 +232,20 @@
|
||||||
calendarDate.getFullYear() === today.getFullYear();
|
calendarDate.getFullYear() === today.getFullYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHoliday(day) {
|
||||||
|
if (!day) return false;
|
||||||
|
return calendarHolidays.has(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHolidayName(day) {
|
||||||
|
if (!day) return null;
|
||||||
|
const info = calendarHolidays.get(day);
|
||||||
|
return info ? info.name : null;
|
||||||
|
}
|
||||||
|
|
||||||
$: calendarDays = getCalendarDays(calendarDate);
|
$: calendarDays = getCalendarDays(calendarDate);
|
||||||
$: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
$: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||||
|
$: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth());
|
||||||
|
|
||||||
// Timezone definitions
|
// Timezone definitions
|
||||||
const timezones = [
|
const timezones = [
|
||||||
|
|
@ -330,7 +343,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="calendar-days">
|
<div class="calendar-days">
|
||||||
{#each calendarDays as day}
|
{#each calendarDays as day}
|
||||||
<span class="calendar-day" class:today={isToday(day)} class:empty={!day}>
|
<span
|
||||||
|
class="calendar-day"
|
||||||
|
class:today={isToday(day)}
|
||||||
|
class:holiday={isHoliday(day)}
|
||||||
|
class:empty={!day}
|
||||||
|
title={getHolidayName(day) || ''}
|
||||||
|
>
|
||||||
{day || ''}
|
{day || ''}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -725,6 +744,24 @@
|
||||||
background: #9eeea1;
|
background: #9eeea1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-day.holiday {
|
||||||
|
background: #ff9800;
|
||||||
|
color: #0d1117;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.holiday:not(.today):hover {
|
||||||
|
background: #ffb74d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today.holiday {
|
||||||
|
background: linear-gradient(135deg, #7ee787 50%, #ff9800 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today.holiday:hover {
|
||||||
|
background: linear-gradient(135deg, #9eeea1 50%, #ffb74d 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.timezone-row {
|
.timezone-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@
|
||||||
let lastSeekTime = 0; // Track last seek time for rate-limiting
|
let lastSeekTime = 0; // Track last seek time for rate-limiting
|
||||||
let leadInEndedAt = 0; // Track when lead-in ended for grace period
|
let leadInEndedAt = 0; // Track when lead-in ended for grace period
|
||||||
let wasInLeadIn = false; // Track previous lead-in state
|
let wasInLeadIn = false; // Track previous lead-in state
|
||||||
|
let restartInProgress = false; // Track when video restart is in progress (prevents drift detection)
|
||||||
|
let restartStartedAt = 0; // When restart started
|
||||||
|
|
||||||
const SYNC_CHECK_INTERVAL = 1000; // Check sync every second (matches server sync interval)
|
const SYNC_CHECK_INTERVAL = 1000; // Check sync every second (matches server sync interval)
|
||||||
|
const RESTART_GRACE_PERIOD = 3000; // 3 seconds grace period during video restart
|
||||||
const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync)
|
const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync)
|
||||||
const MIN_SYNC_INTERVAL = 1000; // Minimum 1 second between sync checks (server pushes every 1s)
|
const MIN_SYNC_INTERVAL = 1000; // Minimum 1 second between sync checks (server pushes every 1s)
|
||||||
const CONTROLLER_SEEK_DEBOUNCE = 2000; // Debounce controller seeks by 2 seconds
|
const CONTROLLER_SEEK_DEBOUNCE = 2000; // Debounce controller seeks by 2 seconds
|
||||||
|
|
@ -122,6 +125,9 @@
|
||||||
// YT.PlayerState: UNSTARTED (-1), ENDED (0), PLAYING (1), PAUSED (2), BUFFERING (3), CUED (5)
|
// YT.PlayerState: UNSTARTED (-1), ENDED (0), PLAYING (1), PAUSED (2), BUFFERING (3), CUED (5)
|
||||||
// Always handle ENDED state - crucial for playlist advancement
|
// Always handle ENDED state - crucial for playlist advancement
|
||||||
if (state === window.YT.PlayerState.ENDED) {
|
if (state === window.YT.PlayerState.ENDED) {
|
||||||
|
// Set restart flag to prevent drift detection during transition
|
||||||
|
restartInProgress = true;
|
||||||
|
restartStartedAt = Date.now();
|
||||||
dispatch('ended');
|
dispatch('ended');
|
||||||
// Also notify server directly - this is a fallback for when duration-based detection fails
|
// Also notify server directly - this is a fallback for when duration-based detection fails
|
||||||
watchSync.videoEnded();
|
watchSync.videoEnded();
|
||||||
|
|
@ -187,10 +193,29 @@
|
||||||
const shouldBePlaying = storeState.playbackState === 'playing';
|
const shouldBePlaying = storeState.playbackState === 'playing';
|
||||||
const hasVideoEnded = playerState === window.YT.PlayerState.ENDED;
|
const hasVideoEnded = playerState === window.YT.PlayerState.ENDED;
|
||||||
|
|
||||||
|
// During restart transition (video ended, waiting for server response), skip drift detection
|
||||||
|
// This prevents false "Controller seek detected" when video loops/restarts
|
||||||
|
if (restartInProgress) {
|
||||||
|
if (now - restartStartedAt >= RESTART_GRACE_PERIOD) {
|
||||||
|
// Grace period expired, reset flag
|
||||||
|
restartInProgress = false;
|
||||||
|
} else {
|
||||||
|
// Still in grace period - only ensure video starts playing if needed, no sync
|
||||||
|
if (shouldBePlaying && !isPlayerPlaying && !isBuffering && !hasVideoEnded) {
|
||||||
|
ignoreStateChange = true;
|
||||||
|
player.playVideo();
|
||||||
|
setTimeout(() => { ignoreStateChange = false; }, 1500);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// During lead-in period, let video buffer without seeking
|
// During lead-in period, let video buffer without seeking
|
||||||
// Server sends leadIn=true for 3 seconds after play starts
|
// Server sends leadIn=true for 3 seconds after play starts
|
||||||
if (storeState.leadIn) {
|
if (storeState.leadIn) {
|
||||||
wasInLeadIn = true;
|
wasInLeadIn = true;
|
||||||
|
// Lead-in started, clear restart flag since server has responded
|
||||||
|
restartInProgress = false;
|
||||||
// During lead-in, just ensure video is loading/buffering
|
// During lead-in, just ensure video is loading/buffering
|
||||||
// Don't seek or sync position - wait for lead-in to complete
|
// Don't seek or sync position - wait for lead-in to complete
|
||||||
if (shouldBePlaying && !isPlayerPlaying && !isBuffering && !hasVideoEnded) {
|
if (shouldBePlaying && !isPlayerPlaying && !isBuffering && !hasVideoEnded) {
|
||||||
|
|
@ -266,6 +291,7 @@
|
||||||
$: if (playerReady && $watchSync.currentVideo?.id !== currentPlaylistItemId) {
|
$: if (playerReady && $watchSync.currentVideo?.id !== currentPlaylistItemId) {
|
||||||
currentPlaylistItemId = $watchSync.currentVideo?.id;
|
currentPlaylistItemId = $watchSync.currentVideo?.id;
|
||||||
durationReportedForItemId = null; // Reset so we report duration for new video
|
durationReportedForItemId = null; // Reset so we report duration for new video
|
||||||
|
restartInProgress = false; // Video changed, clear restart flag
|
||||||
const newVideoId = $watchSync.currentVideo?.youtubeVideoId;
|
const newVideoId = $watchSync.currentVideo?.youtubeVideoId;
|
||||||
if (newVideoId && player) {
|
if (newVideoId && player) {
|
||||||
videoId = newVideoId;
|
videoId = newVideoId;
|
||||||
|
|
@ -302,13 +328,18 @@
|
||||||
} else if (action === 'seek' && currentTime !== undefined) {
|
} else if (action === 'seek' && currentTime !== undefined) {
|
||||||
player.seekTo(currentTime, true);
|
player.seekTo(currentTime, true);
|
||||||
} else if (action === 'skip' || action === 'video_changed') {
|
} else if (action === 'skip' || action === 'video_changed') {
|
||||||
|
// Server has responded, clear restart flag
|
||||||
|
restartInProgress = false;
|
||||||
// Video change will be handled by the reactive statement above
|
// Video change will be handled by the reactive statement above
|
||||||
setTimeout(() => checkAndSync(true), 1000);
|
setTimeout(() => checkAndSync(true), 1000);
|
||||||
} else if (action === 'locked_restart' || action === 'repeat') {
|
} else if (action === 'locked_restart' || action === 'repeat') {
|
||||||
|
// Server has responded with restart, clear the restart flag
|
||||||
|
restartInProgress = false;
|
||||||
// Locked video loop - seek to beginning and play
|
// Locked video loop - seek to beginning and play
|
||||||
player.seekTo(0, true);
|
player.seekTo(0, true);
|
||||||
player.playVideo();
|
player.playVideo();
|
||||||
setTimeout(() => checkAndSync(true), 1000);
|
// Delay sync check longer to allow player to stabilize
|
||||||
|
setTimeout(() => checkAndSync(true), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => { ignoreStateChange = false; }, 1000);
|
setTimeout(() => { ignoreStateChange = false; }, 1000);
|
||||||
|
|
|
||||||
110
frontend/src/lib/data/holidays.js
Normal file
110
frontend/src/lib/data/holidays.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
* Holiday data for terminal calendar
|
||||||
|
* Includes both Indian national holidays and international holidays
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed holidays - same date every year
|
||||||
|
* Format: { month: 0-11, day: 1-31, name: string, type: 'indian' | 'international' }
|
||||||
|
*/
|
||||||
|
export const fixedHolidays = [
|
||||||
|
// Indian National Holidays (Fixed)
|
||||||
|
{ month: 0, day: 26, name: 'Republic Day', type: 'indian' },
|
||||||
|
{ month: 7, day: 15, name: 'Independence Day', type: 'indian' },
|
||||||
|
{ month: 9, day: 2, name: 'Gandhi Jayanti', type: 'indian' },
|
||||||
|
|
||||||
|
// International Holidays (Fixed)
|
||||||
|
{ month: 0, day: 1, name: "New Year's Day", type: 'international' },
|
||||||
|
{ month: 1, day: 14, name: "Valentine's Day", type: 'international' },
|
||||||
|
{ month: 9, day: 31, name: 'Halloween', type: 'international' },
|
||||||
|
{ month: 11, day: 25, name: 'Christmas', type: 'international' },
|
||||||
|
{ month: 11, day: 31, name: "New Year's Eve", type: 'international' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable holidays - different date each year (lunar calendar based)
|
||||||
|
* Format: { [year]: Array<{ month: 0-11, day: 1-31, name: string, type: string }> }
|
||||||
|
*/
|
||||||
|
export const variableHolidays = {
|
||||||
|
2024: [
|
||||||
|
{ month: 2, day: 25, name: 'Holi', type: 'indian' },
|
||||||
|
{ month: 3, day: 10, name: 'Eid ul-Fitr', type: 'indian' },
|
||||||
|
{ month: 5, day: 17, name: 'Eid ul-Adha', type: 'indian' },
|
||||||
|
{ month: 7, day: 19, name: 'Raksha Bandhan', type: 'indian' },
|
||||||
|
{ month: 7, day: 26, name: 'Janmashtami', type: 'indian' },
|
||||||
|
{ month: 8, day: 7, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||||
|
{ month: 9, day: 12, name: 'Dussehra', type: 'indian' },
|
||||||
|
{ month: 10, day: 1, name: 'Diwali', type: 'indian' },
|
||||||
|
],
|
||||||
|
2025: [
|
||||||
|
{ month: 2, day: 14, name: 'Holi', type: 'indian' },
|
||||||
|
{ month: 2, day: 31, name: 'Eid ul-Fitr', type: 'indian' },
|
||||||
|
{ month: 5, day: 7, name: 'Eid ul-Adha', type: 'indian' },
|
||||||
|
{ month: 7, day: 9, name: 'Raksha Bandhan', type: 'indian' },
|
||||||
|
{ month: 7, day: 16, name: 'Janmashtami', type: 'indian' },
|
||||||
|
{ month: 7, day: 27, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||||
|
{ month: 9, day: 2, name: 'Dussehra', type: 'indian' },
|
||||||
|
{ month: 9, day: 20, name: 'Diwali', type: 'indian' },
|
||||||
|
],
|
||||||
|
2026: [
|
||||||
|
{ month: 2, day: 4, name: 'Holi', type: 'indian' },
|
||||||
|
{ month: 2, day: 20, name: 'Eid ul-Fitr', type: 'indian' },
|
||||||
|
{ month: 4, day: 27, name: 'Eid ul-Adha', type: 'indian' },
|
||||||
|
{ month: 7, day: 28, name: 'Raksha Bandhan', type: 'indian' },
|
||||||
|
{ month: 8, day: 4, name: 'Janmashtami', type: 'indian' },
|
||||||
|
{ month: 8, day: 17, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||||
|
{ month: 8, day: 20, name: 'Dussehra', type: 'indian' },
|
||||||
|
{ month: 10, day: 8, name: 'Diwali', type: 'indian' },
|
||||||
|
],
|
||||||
|
2027: [
|
||||||
|
{ month: 2, day: 22, name: 'Holi', type: 'indian' },
|
||||||
|
{ month: 2, day: 10, name: 'Eid ul-Fitr', type: 'indian' },
|
||||||
|
{ month: 4, day: 17, name: 'Eid ul-Adha', type: 'indian' },
|
||||||
|
{ month: 7, day: 17, name: 'Raksha Bandhan', type: 'indian' },
|
||||||
|
{ month: 7, day: 25, name: 'Janmashtami', type: 'indian' },
|
||||||
|
{ month: 8, day: 6, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||||
|
{ month: 9, day: 9, name: 'Dussehra', type: 'indian' },
|
||||||
|
{ month: 9, day: 28, name: 'Diwali', type: 'indian' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all holidays for a specific month and year
|
||||||
|
* @param {number} year - Full year (e.g., 2024)
|
||||||
|
* @param {number} month - Month (0-11)
|
||||||
|
* @returns {Map<number, { name: string, type: string }>} - Map of day -> holiday info
|
||||||
|
*/
|
||||||
|
export function getHolidaysForMonth(year, month) {
|
||||||
|
const holidayMap = new Map();
|
||||||
|
|
||||||
|
// Add fixed holidays for this month
|
||||||
|
for (const holiday of fixedHolidays) {
|
||||||
|
if (holiday.month === month) {
|
||||||
|
holidayMap.set(holiday.day, { name: holiday.name, type: holiday.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add variable holidays for this year and month
|
||||||
|
const yearHolidays = variableHolidays[year];
|
||||||
|
if (yearHolidays) {
|
||||||
|
for (const holiday of yearHolidays) {
|
||||||
|
if (holiday.month === month) {
|
||||||
|
holidayMap.set(holiday.day, { name: holiday.name, type: holiday.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return holidayMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific date is a holiday
|
||||||
|
* @param {number} year - Full year
|
||||||
|
* @param {number} month - Month (0-11)
|
||||||
|
* @param {number} day - Day of month (1-31)
|
||||||
|
* @returns {{ name: string, type: string } | null}
|
||||||
|
*/
|
||||||
|
export function getHolidayInfo(year, month, day) {
|
||||||
|
const monthHolidays = getHolidaysForMonth(year, month);
|
||||||
|
return monthHolidays.get(day) || null;
|
||||||
|
}
|
||||||
|
|
@ -203,33 +203,31 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<style>
|
<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 {
|
.stream-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 2rem;
|
gap: 1.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.stream-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.stream-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.stream-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.stream-card {
|
.stream-card {
|
||||||
background: #111;
|
background: #111;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|
@ -979,11 +977,6 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero">
|
|
||||||
<h1>Live Streams</h1>
|
|
||||||
<p>Watch your favorite streamers live</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div style="text-align: center; padding: 2rem;">
|
<div style="text-align: center; padding: 2rem;">
|
||||||
<p>Loading streams...</p>
|
<p>Loading streams...</p>
|
||||||
|
|
@ -1036,6 +1029,61 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 Videos Section -->
|
<!-- Latest Videos Section -->
|
||||||
{#if !videosLoading && videos.length > 0}
|
{#if !videosLoading && videos.length > 0}
|
||||||
<div class="videos-section">
|
<div class="videos-section">
|
||||||
|
|
@ -1132,61 +1180,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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 -->
|
<!-- Latest Ebooks Section -->
|
||||||
{#if !ebooksLoading && ebookFiles.length > 0}
|
{#if !ebooksLoading && ebookFiles.length > 0}
|
||||||
<div class="ebooks-section">
|
<div class="ebooks-section">
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { connectionStatus } from '$lib/chat/chatStore';
|
import { connectionStatus } from '$lib/chat/chatStore';
|
||||||
import { auth, isAuthenticated } from '$lib/stores/auth';
|
import { auth, isAuthenticated } from '$lib/stores/auth';
|
||||||
|
import { getHolidaysForMonth } from '$lib/data/holidays.js';
|
||||||
import { siteSettings } from '$lib/stores/siteSettings';
|
import { siteSettings } from '$lib/stores/siteSettings';
|
||||||
import TerminalTabBar from '$lib/components/terminal/TerminalTabBar.svelte';
|
import TerminalTabBar from '$lib/components/terminal/TerminalTabBar.svelte';
|
||||||
import TerminalCore from '$lib/components/terminal/TerminalCore.svelte';
|
import TerminalCore from '$lib/components/terminal/TerminalCore.svelte';
|
||||||
|
|
@ -129,8 +130,20 @@
|
||||||
calendarDate.getFullYear() === today.getFullYear();
|
calendarDate.getFullYear() === today.getFullYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHoliday(day) {
|
||||||
|
if (!day) return false;
|
||||||
|
return calendarHolidays.has(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHolidayName(day) {
|
||||||
|
if (!day) return null;
|
||||||
|
const info = calendarHolidays.get(day);
|
||||||
|
return info ? info.name : null;
|
||||||
|
}
|
||||||
|
|
||||||
$: calendarDays = getCalendarDays(calendarDate);
|
$: calendarDays = getCalendarDays(calendarDate);
|
||||||
$: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
$: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||||
|
$: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth());
|
||||||
|
|
||||||
// Timezone definitions
|
// Timezone definitions
|
||||||
const timezones = [
|
const timezones = [
|
||||||
|
|
@ -224,7 +237,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="calendar-days">
|
<div class="calendar-days">
|
||||||
{#each calendarDays as day}
|
{#each calendarDays as day}
|
||||||
<span class="calendar-day" class:today={isToday(day)} class:empty={!day}>
|
<span
|
||||||
|
class="calendar-day"
|
||||||
|
class:today={isToday(day)}
|
||||||
|
class:holiday={isHoliday(day)}
|
||||||
|
class:empty={!day}
|
||||||
|
title={getHolidayName(day) || ''}
|
||||||
|
>
|
||||||
{day || ''}
|
{day || ''}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -534,6 +553,24 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-day.holiday {
|
||||||
|
background: #ff9800;
|
||||||
|
color: #0d1117;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.holiday:not(.today):hover {
|
||||||
|
background: #ffb74d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today.holiday {
|
||||||
|
background: linear-gradient(135deg, #4caf50 50%, #ff9800 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today.holiday:hover {
|
||||||
|
background: linear-gradient(135deg, #66bb6a 50%, #ffb74d 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue