Files
Barış Keserandgoogle-labs-jules[bot] 099ef897a8 🎨 Palette: Add focus-visible styles for keyboard accessibility (#182)
- Added `focus-visible:ring-2` to buttons, links, and inputs across templates.
- Added `focus-within` to anime cards.
- Added `aria-label` to chat inputs and send buttons.
- Documented learning in `.jules/palette.md`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-02-28 20:12:30 +00:00

203 lines
9.6 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<div class="max-w-7xl mx-auto px-4">
<div class="mb-4">
<h1 class="text-3xl font-bold text-white">{{ episode.season.anime.title }}</h1>
<h2 class="text-xl text-gray-400">Season {{ episode.season.number }} - Episode {{ episode.number }} {% if episode.title %}: {{ episode.title }}{% endif %}</h2>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Video Player Column (2/3 width) -->
<div class="lg:col-span-2">
<div class="relative w-full pb-[56.25%] bg-black rounded-xl overflow-hidden shadow-2xl border border-gray-800">
{% if video %}
<video id="player" playsinline controls class="absolute top-0 left-0 w-full h-full"></video>
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
<script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const video = document.getElementById('player');
// Construct URL assuming standard media structure
const source = "/media/videos/{{ episode.id }}/{{ video.quality }}/index.m3u8";
if (Hls.isSupported()) {
const hls = new Hls({
xhrSetup: function(xhr, url) {
// Prepare for key loading with cookies/auth if needed
xhr.withCredentials = true;
}
});
hls.loadSource(source);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = source;
}
const player = new Plyr(video, {
controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
});
});
</script>
{% else %}
<div class="absolute top-0 left-0 w-full h-full flex items-center justify-center text-red-500 font-bold bg-gray-900">
VIDEO SOURCE NOT FOUND
</div>
{% endif %}
</div>
<div class="mt-6 p-4 bg-gray-800 rounded-lg">
<h3 class="font-bold text-lg mb-2">Details</h3>
<p class="text-gray-400">{{ episode.season.anime.synopsis|truncatewords:50 }}</p>
{% if video.fansub_group %}
<p class="mt-2 text-sm text-brand">Fansub: {{ video.fansub_group.name }}</p>
{% endif %}
<div class="mt-4 pt-4 border-t border-gray-700">
<h4 class="font-bold text-sm mb-2 text-white">Actions</h4>
<a href="{% url 'create_watch_party' episode.id %}" class="inline-block bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded transition text-sm font-semibold focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 focus-visible:ring-brand">
🎉 Start Watch Party
</a>
</div>
</div>
</div>
<!-- Chat Column (1/3 width) -->
<div class="bg-gray-800 rounded-xl border border-gray-700 flex flex-col h-[600px] overflow-hidden">
<div class="p-4 border-b border-gray-700 bg-gray-900 flex justify-between items-center">
<div>
<h3 class="font-bold text-white">Watch Party Chat</h3>
<p class="text-xs text-gray-400">Connected as {{ user.username|default:"Anonymous" }}</p>
</div>
<div class="flex items-center gap-1.5 px-2 py-1 bg-gray-800 rounded-lg border border-gray-700">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span id="online-count" class="text-xs font-mono text-gray-300">0</span>
</div>
</div>
<div id="chat-messages" class="flex-1 p-4 overflow-y-auto space-y-2 bg-gray-800 scrollbar-thin scrollbar-thumb-gray-600 scrollbar-track-gray-800">
<!-- Messages will appear here -->
</div>
<div class="p-3 bg-gray-900 border-t border-gray-700">
<div class="flex gap-2">
<input type="text" id="chat-input"
class="flex-1 bg-gray-700 text-white rounded-lg px-3 py-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
aria-label="Chat message"
placeholder="Say something..."
{% if not user.is_authenticated %}disabled title="Login to chat"{% endif %}
>
<button id="chat-send"
class="bg-brand text-white px-4 py-2 rounded-lg hover:bg-brand-dark transition disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900"
aria-label="Send message"
{% if not user.is_authenticated %}disabled{% endif %}
>
Send
</button>
</div>
{% if not user.is_authenticated %}
<p class="text-xs text-center text-red-400 mt-2">Please login to chat.</p>
{% endif %}
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const roomId = "episode_{{ episode.id }}";
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const chatSocket = new WebSocket(
protocol + '//' + window.location.host + '/ws/chat/' + roomId + '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
// Handle user count updates
if (data.type === 'user_count') {
const countElement = document.getElementById('online-count');
if (countElement) {
countElement.textContent = data.count;
}
return;
}
// Handle chat messages
const messages = document.getElementById('chat-messages');
const messageItem = document.createElement('div');
if (data.is_system) {
messageItem.className = 'text-center text-xs text-gray-500 italic my-2 animate-fade-in';
messageItem.textContent = data.message;
} else {
messageItem.className = 'bg-gray-700/50 p-2 rounded animate-fade-in';
const meta = document.createElement('div');
meta.className = 'flex justify-between text-xs text-gray-400 mb-1';
const userSpan = document.createElement('span');
userSpan.className = 'font-bold text-blue-400';
userSpan.textContent = data.username;
const timeSpan = document.createElement('span');
const date = new Date(data.created_at || Date.now());
timeSpan.textContent = date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
meta.appendChild(userSpan);
meta.appendChild(timeSpan);
const textDiv = document.createElement('div');
textDiv.className = 'text-sm text-gray-200 break-words';
textDiv.textContent = data.message;
messageItem.appendChild(meta);
messageItem.appendChild(textDiv);
}
messages.appendChild(messageItem);
messages.scrollTop = messages.scrollHeight;
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
const messages = document.getElementById('chat-messages');
const errorMsg = document.createElement('div');
errorMsg.className = 'text-center text-red-500 text-xs mt-2';
errorMsg.textContent = 'Connection lost. Please refresh.';
messages.appendChild(errorMsg);
};
const chatInput = document.querySelector('#chat-input');
const chatSend = document.querySelector('#chat-send');
if (chatInput && chatSend) {
chatInput.focus();
chatInput.onkeyup = function(e) {
if (e.key === 'Enter') {
chatSend.click();
}
};
chatSend.onclick = function(e) {
const message = chatInput.value;
if (message) {
chatSocket.send(JSON.stringify({
'message': message,
// username is handled by backend from session if auth, else 'Anonymous' or generic
}));
chatInput.value = '';
}
};
}
});
</script>
{% endblock %}