kv-tube/templates/my_videos.html

463 lines
No EOL
18 KiB
HTML

{% extends "layout.html" %}
{% block content %}
<style>
/* Library Page Premium Styles */
.library-container {
padding: 24px;
max-width: 1600px;
margin: 0 auto;
}
.library-header {
margin-bottom: 2rem;
text-align: center;
}
.library-title {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, var(--yt-text-primary) 0%, var(--yt-text-secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.library-tabs {
display: inline-flex;
gap: 8px;
background: var(--yt-bg-secondary);
padding: 6px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
margin-bottom: 1rem;
}
.library-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 600;
color: var(--yt-text-secondary);
text-decoration: none;
transition: all 0.25s ease;
border: none;
background: transparent;
cursor: pointer;
}
.library-tab:hover {
color: var(--yt-text-primary);
background: var(--yt-bg-hover);
}
.library-tab.active {
background: linear-gradient(135deg, #cc0000 0%, #ff4444 100%);
color: white;
box-shadow: 0 4px 12px rgba(204, 0, 0, 0.3);
}
.library-tab i {
font-size: 1rem;
}
.library-actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 16px;
}
.clear-btn {
display: none;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: transparent;
border: 1px solid var(--yt-border);
border-radius: 24px;
color: var(--yt-text-secondary);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.clear-btn:hover {
background: rgba(204, 0, 0, 0.1);
border-color: #cc0000;
color: #cc0000;
}
.library-stats {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 12px;
color: var(--yt-text-secondary);
font-size: 0.85rem;
}
.library-stat {
display: flex;
align-items: center;
gap: 6px;
}
.library-stat i {
opacity: 0.7;
}
/* Empty State Enhancement */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--yt-text-secondary);
}
.empty-state-icon {
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
border-radius: 50%;
background: var(--yt-bg-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
opacity: 0.6;
}
.empty-state h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
color: var(--yt-text-primary);
}
.empty-state p {
margin-bottom: 1.5rem;
}
.browse-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: linear-gradient(135deg, #cc0000 0%, #ff4444 100%);
color: white;
border: none;
border-radius: 24px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(204, 0, 0, 0.3);
}
.browse-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(204, 0, 0, 0.4);
}
</style>
<div class="library-container">
<div class="library-header">
<h1 class="library-title">My Library</h1>
<div class="library-tabs">
<a href="/my-videos?type=history" class="library-tab" id="tab-history">
<i class="fas fa-history"></i>
<span>History</span>
</a>
<a href="/my-videos?type=saved" class="library-tab" id="tab-saved">
<i class="fas fa-bookmark"></i>
<span>Saved</span>
</a>
<a href="/my-videos?type=subscriptions" class="library-tab" id="tab-subscriptions">
<i class="fas fa-users"></i>
<span>Subscriptions</span>
</a>
</div>
<div class="library-stats" id="libraryStats" style="display: none;">
<div class="library-stat">
<i class="fas fa-video"></i>
<span id="videoCount">0 videos</span>
</div>
</div>
<div class="library-actions">
<button id="clearBtn" onclick="clearLibrary()" class="clear-btn">
<i class="fas fa-trash-alt"></i>
<span>Clear <span id="clearType">All</span></span>
</button>
</div>
</div>
<!-- Video Grid -->
<div id="libraryGrid" class="yt-video-grid">
<!-- JS will populate this -->
</div>
<!-- Empty State -->
<div id="emptyState" class="empty-state" style="display: none;">
<div class="empty-state-icon">
<i class="fas fa-folder-open"></i>
</div>
<h3>Nothing here yet</h3>
<p id="emptyMsg">Go watch some videos to fill this up!</p>
<a href="/" class="browse-btn">
<i class="fas fa-play"></i>
Browse Content
</a>
</div>
</div>
<script>
// Load library content - extracted to function for reuse on pageshow
function loadLibraryContent() {
const urlParams = new URLSearchParams(window.location.search);
// Default to history if no type or invalid type
const type = urlParams.get('type') || 'history';
// Reset all tabs first, then activate the correct one
document.querySelectorAll('.library-tab').forEach(tab => tab.classList.remove('active'));
const activeTab = document.getElementById(`tab-${type}`);
if (activeTab) {
activeTab.classList.add('active');
}
const grid = document.getElementById('libraryGrid');
const empty = document.getElementById('emptyState');
const emptyMsg = document.getElementById('emptyMsg');
const statsDiv = document.getElementById('libraryStats');
const clearBtn = document.getElementById('clearBtn');
// Reset UI before loading
grid.innerHTML = '';
empty.style.display = 'none';
if (statsDiv) statsDiv.style.display = 'none';
if (clearBtn) clearBtn.style.display = 'none';
// Mapping URL type to localStorage key suffix
// saved -> kv_saved
// history -> kv_history
// subscriptions -> kv_subscriptions
const storageKey = `kv_${type}`;
const data = JSON.parse(localStorage.getItem(storageKey) || '[]').filter(i => i && i.id);
// Show stats and Clear Button if there is data
if (data.length > 0) {
empty.style.display = 'none';
// Update stats
const videoCount = document.getElementById('videoCount');
if (statsDiv && videoCount) {
statsDiv.style.display = 'flex';
const countText = type === 'subscriptions'
? `${data.length} channel${data.length !== 1 ? 's' : ''}`
: `${data.length} video${data.length !== 1 ? 's' : ''}`;
videoCount.innerText = countText;
}
const clearTypeSpan = document.getElementById('clearType');
if (clearBtn) {
clearBtn.style.display = 'inline-flex';
// Format type name for display
const typeName = type.charAt(0).toUpperCase() + type.slice(1);
clearTypeSpan.innerText = typeName;
}
if (type === 'subscriptions') {
// Render Channel Cards with improved design
grid.style.display = 'grid';
grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(200px, 1fr))';
grid.style.gap = '24px';
grid.style.padding = '20px 0';
grid.innerHTML = data.map(channel => {
const avatarHtml = channel.thumbnail
? `<img src="${channel.thumbnail}" style="width:120px; height:120px; border-radius:50%; object-fit:cover; border: 3px solid var(--yt-border); transition: transform 0.3s, border-color 0.3s;">`
: `<div style="width:120px; height:120px; border-radius:50%; background: linear-gradient(135deg, #FF6B6B 0%, #d62d2d 100%); display:flex; align-items:center; justify-content:center; font-size:48px; font-weight:bold; color:white; border: 3px solid var(--yt-border); transition: transform 0.3s;">${channel.letter || channel.title.charAt(0).toUpperCase()}</div>`;
return `
<div class="subscription-card" onclick="window.location.href='/channel/${channel.id}'"
style="text-align:center; cursor:pointer; padding: 24px 16px; background: var(--yt-bg-secondary); border-radius: 16px; transition: all 0.3s; border: 1px solid transparent;"
onmouseenter="this.style.transform='translateY(-4px)'; this.style.boxShadow='0 8px 24px rgba(0,0,0,0.3)'; this.style.borderColor='var(--yt-border)';"
onmouseleave="this.style.transform='none'; this.style.boxShadow='none'; this.style.borderColor='transparent';">
<div style="display:flex; justify-content:center; margin-bottom:16px;">
${avatarHtml}
</div>
<h3 style="font-size:1.1rem; margin-bottom:8px; color: var(--yt-text-primary); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${channel.title}</h3>
<p style="font-size: 0.85rem; color: var(--yt-text-secondary); margin-bottom: 12px;">@${channel.title.replace(/\s+/g, '')}</p>
<button onclick="event.stopPropagation(); toggleSubscribe('${channel.id}', '${channel.title.replace(/'/g, "\\'")}', '${channel.thumbnail || ''}', this)"
style="padding:10px 20px; font-size:13px; background: linear-gradient(135deg, #cc0000, #ff4444); color: white; border: none; border-radius: 24px; cursor: pointer; font-weight: 600; transition: all 0.2s; box-shadow: 0 2px 8px rgba(204,0,0,0.3);"
onmouseenter="this.style.transform='scale(1.05)'; this.style.boxShadow='0 4px 12px rgba(204,0,0,0.5)';"
onmouseleave="this.style.transform='scale(1)'; this.style.boxShadow='0 2px 8px rgba(204,0,0,0.3)';">
<i class="fas fa-user-minus"></i> Unsubscribe
</button>
</div>
`}).join('');
} else {
// Render Video Cards (History/Saved)
grid.innerHTML = data.map(video => {
// Robust fallback chain: maxres -> hq -> mq
const thumb = video.thumbnail || `https://i.ytimg.com/vi/${video.id}/maxresdefault.jpg`;
const showRemove = type === 'saved' || type === 'history';
return `
<div class="yt-video-card" style="position: relative;">
<div onclick="window.location.href='/watch?v=${video.id}'" style="cursor: pointer;">
<div class="yt-thumbnail-container">
<img src="${thumb}" class="yt-thumbnail" loading="lazy" referrerpolicy="no-referrer"
onload="this.classList.add('loaded')"
onerror="
if (this.src.includes('maxresdefault')) this.src='https://i.ytimg.com/vi/${video.id}/hqdefault.jpg';
else if (this.src.includes('hqdefault')) this.src='https://i.ytimg.com/vi/${video.id}/mqdefault.jpg';
else this.style.display='none';
">
<div class="yt-duration">${video.duration || ''}</div>
</div>
<div class="yt-video-details">
<div class="yt-video-meta">
<h3 class="yt-video-title">${video.title}</h3>
<p class="yt-video-stats">${video.uploader}</p>
</div>
</div>
</div>
${showRemove ? `
<button onclick="event.stopPropagation(); removeVideo('${video.id}', '${type}', this)"
style="position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; background: rgba(0,0,0,0.7); color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; opacity: 0.8; transition: all 0.2s; z-index: 10;"
onmouseenter="this.style.opacity='1'; this.style.background='#cc0000';"
onmouseleave="this.style.opacity='0.8'; this.style.background='rgba(0,0,0,0.7)';"
title="Remove">
<i class="fas fa-times"></i>
</button>` : ''}
</div>
`}).join('');
}
} else {
grid.innerHTML = '';
empty.style.display = 'block';
if (type === 'subscriptions') {
emptyMsg.innerText = "You haven't subscribed to any channels yet.";
} else if (type === 'saved') {
emptyMsg.innerText = "No saved videos yet.";
}
}
}
// Run on initial page load and SPA navigation
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
loadLibraryContent();
initTabs();
});
} else {
// Document already loaded (SPA navigation)
loadLibraryContent();
initTabs();
}
function initTabs() {
// Intercept tab clicks for client-side navigation
document.querySelectorAll('.library-tab').forEach(tab => {
// Remove old listeners to be safe (optional but good practice in SPA)
const newTab = tab.cloneNode(true);
tab.parentNode.replaceChild(newTab, tab);
newTab.addEventListener('click', (e) => {
e.preventDefault();
const newUrl = newTab.getAttribute('href');
// Update URL without reloading
history.pushState(null, '', newUrl);
// Immediately load the new content
loadLibraryContent();
});
});
}
// Handle browser back/forward buttons
window.addEventListener('popstate', () => {
loadLibraryContent();
});
function clearLibrary() {
const urlParams = new URLSearchParams(window.location.search);
const type = urlParams.get('type') || 'history';
const typeName = type.charAt(0).toUpperCase() + type.slice(1);
if (confirm(`Are you sure you want to clear your ${typeName}? This cannot be undone.`)) {
const storageKey = `kv_${type}`;
localStorage.removeItem(storageKey);
// Reload to reflect changes
window.location.reload();
}
}
// Local toggleSubscribe for my_videos page - removes card visually
function toggleSubscribe(channelId, channelName, avatar, btnElement) {
event.stopPropagation();
// Remove from library
const key = 'kv_subscriptions';
let data = JSON.parse(localStorage.getItem(key) || '[]');
data = data.filter(item => item.id !== channelId);
localStorage.setItem(key, JSON.stringify(data));
// Remove the card from UI
const card = btnElement.closest('.yt-channel-card');
if (card) {
card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0';
card.style.transform = 'scale(0.8)';
setTimeout(() => card.remove(), 300);
}
// Show empty state if no more subscriptions
setTimeout(() => {
const grid = document.getElementById('libraryGrid');
if (grid && grid.children.length === 0) {
grid.innerHTML = '';
document.getElementById('emptyState').style.display = 'block';
document.getElementById('emptyMessage').innerText = "You haven't subscribed to any channels yet.";
}
}, 350);
}
// Remove individual video from saved/history
function removeVideo(videoId, type, btnElement) {
event.stopPropagation();
const key = `kv_${type}`;
let data = JSON.parse(localStorage.getItem(key) || '[]');
data = data.filter(item => item.id !== videoId);
localStorage.setItem(key, JSON.stringify(data));
// Remove the card from UI with animation
const card = btnElement.closest('.yt-video-card');
if (card) {
card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0';
card.style.transform = 'scale(0.9)';
setTimeout(() => card.remove(), 300);
}
// Show empty state if no more videos
setTimeout(() => {
const grid = document.getElementById('libraryGrid');
if (grid && grid.children.length === 0) {
grid.innerHTML = '';
document.getElementById('emptyState').style.display = 'block';
const typeName = type === 'saved' ? 'No saved videos yet.' : 'No history yet.';
document.getElementById('emptyMessage').innerText = typeName;
}
}, 350);
}
</script>
{% endblock %}