450 lines
No EOL
18 KiB
HTML
450 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
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadLibraryContent();
|
|
|
|
// Intercept tab clicks for client-side navigation
|
|
document.querySelectorAll('.library-tab').forEach(tab => {
|
|
tab.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const newUrl = tab.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 %} |