mirror of
https://github.com/barkeser2002/offline-db.git
synced 2026-09-25 01:40:07 +03:00
- 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>
203 lines
9.6 KiB
HTML
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 %}
|