kv-tube/templates/index.html
2026-01-01 20:08:35 +07:00

512 lines
No EOL
16 KiB
HTML

{% extends "layout.html" %}
{% block content %}
<script>
window.APP_CONFIG = {
page: '{{ page|default("home") }}',
channelId: '{{ channel_id|default("") }}',
query: '{{ query|default("") }}'
};
</script>
<!-- Filters & Categories -->
<div class="yt-filter-bar">
<div class="yt-categories" id="categoryList">
<!-- Pinned Categories -->
<button class="yt-chip" onclick="switchCategory('history', this)"><i class="fas fa-history"></i> Watched</button>
<button class="yt-chip" onclick="switchCategory('suggested', this)"><i class="fas fa-magic"></i> Suggested</button>
<!-- Standard Categories -->
<button class="yt-chip" onclick="switchCategory('tech', this)">Tech</button>
<button class="yt-chip" onclick="switchCategory('music', this)">Music</button>
<button class="yt-chip" onclick="switchCategory('movies', this)">Movies</button>
<button class="yt-chip" onclick="switchCategory('news', this)">News</button>
<button class="yt-chip" onclick="switchCategory('trending', this)">Trending</button>
<button class="yt-chip" onclick="switchCategory('podcasts', this)">Podcasts</button>
<button class="yt-chip" onclick="switchCategory('live', this)">Live</button>
<button class="yt-chip" onclick="switchCategory('gaming', this)">Gaming</button>
<button class="yt-chip" onclick="switchCategory('sports', this)">Sports</button>
</div>
<div class="yt-filter-actions">
<div class="yt-dropdown">
<button class="yt-icon-btn" id="filterToggleBtn" onclick="toggleFilterMenu()">
<i class="fas fa-sliders-h"></i>
</button>
<div class="yt-dropdown-menu" id="filterMenu">
<div class="yt-menu-section">
<h4>Sort By</h4>
<button onclick="changeSort('day')">Today</button>
<button onclick="changeSort('week')">This Week</button>
<button onclick="changeSort('month')">This Month</button>
<button onclick="changeSort('3months')">Last 3 Months</button>
<button onclick="changeSort('year')">This Year</button>
</div>
<div class="yt-menu-section">
<h4>Region</h4>
<button onclick="changeRegion('vietnam')">Vietnam</button>
<button onclick="changeRegion('global')">Global</button>
</div>
</div>
</div>
</div>
</div>
<!-- Shorts Section -->
<!-- Videos Section -->
<div id="videosSection" class="yt-section">
<div class="yt-section-header" style="display:none;">
<h2><i class="fas fa-play-circle"></i> Videos</h2>
</div>
<div id="resultsArea" class="yt-video-grid">
<!-- Initial Skeleton State -->
<!-- Initial Skeleton State (12 items) -->
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
<div class="yt-video-card skeleton-card">
<div class="skeleton-thumb skeleton"></div>
<div class="skeleton-details">
<div class="skeleton-avatar skeleton"></div>
<div class="skeleton-text">
<div class="skeleton-title skeleton"></div>
<div class="skeleton-meta skeleton"></div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Filter Bar Styles */
.yt-filter-bar {
display: flex;
align-items: center;
gap: 1rem;
padding: 0 1rem;
margin-bottom: 1rem;
position: sticky;
top: 56px;
/* Adjust based on header height */
z-index: 99;
background: var(--yt-bg-primary);
border-bottom: 1px solid var(--yt-border);
}
.yt-categories {
display: flex;
overflow-x: auto;
gap: 0.8rem;
padding: 0.5rem 0;
flex: 1;
scrollbar-width: none;
/* Firefox */
}
.yt-categories::-webkit-scrollbar {
display: none;
}
.yt-chip {
padding: 0.5rem 1rem;
border-radius: 8px;
background: var(--yt-bg-secondary);
color: var(--yt-text-primary);
border: none;
white-space: nowrap;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
.yt-chip:hover {
background: var(--yt-bg-hover);
}
.yt-chip-active {
background: var(--yt-text-primary);
color: var(--yt-bg-primary);
}
.yt-chip-active:hover {
background: var(--yt-text-primary);
opacity: 0.9;
}
.yt-filter-actions {
flex-shrink: 0;
position: relative;
}
.yt-dropdown-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
width: 200px;
background: var(--yt-bg-secondary);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
margin-top: 0.5rem;
z-index: 100;
border: 1px solid var(--yt-border);
}
.yt-dropdown-menu.show {
display: block;
}
.yt-menu-section {
margin-bottom: 1rem;
}
.yt-menu-section:last-child {
margin-bottom: 0;
}
.yt-menu-section h4 {
font-size: 0.8rem;
color: var(--yt-text-secondary);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.yt-menu-section button {
display: block;
width: 100%;
text-align: left;
padding: 0.5rem;
background: none;
border: none;
color: var(--yt-text-primary);
cursor: pointer;
border-radius: 6px;
}
.yt-menu-section button:hover {
background: var(--yt-bg-hover);
}
/* Shorts Section Styles */
.yt-section {
margin-bottom: 32px;
padding: 0 16px;
}
.yt-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.yt-section-header h2 {
font-size: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.yt-section-header h2 i {
color: var(--yt-accent-red);
}
.yt-shorts-container {
position: relative;
display: flex;
align-items: center;
}
.yt-shorts-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--yt-bg-primary);
border: 1px solid var(--yt-border);
color: var(--yt-text-primary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.yt-shorts-arrow:hover {
background: var(--yt-bg-secondary);
transform: translateY(-50%) scale(1.1);
}
.yt-shorts-left {
left: -20px;
}
.yt-shorts-right {
right: -20px;
}
.yt-shorts-grid {
display: flex;
gap: 12px;
overflow-x: auto;
padding: 8px 0;
scroll-behavior: smooth;
scrollbar-width: none;
flex: 1;
}
.yt-shorts-grid::-webkit-scrollbar {
display: none;
}
.yt-short-card {
flex-shrink: 0;
width: 180px;
cursor: pointer;
transition: transform 0.2s;
}
.yt-short-card:hover {
transform: scale(1.02);
}
.yt-short-thumb {
width: 180px;
height: 320px;
border-radius: 12px;
object-fit: cover;
background: var(--yt-bg-secondary);
opacity: 0;
transition: opacity 0.5s ease;
}
.yt-short-thumb.loaded {
opacity: 1;
.yt-short-title {
font-size: 14px;
font-weight: 500;
margin-top: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.yt-short-views {
font-size: 12px;
color: var(--yt-text-secondary);
margin-top: 4px;
}
@media (max-width: 768px) {
.yt-shorts-arrow {
display: none;
}
.yt-filter-bar {
padding: 0 10px;
top: 56px;
}
.yt-sort-container {
/* Legacy override if needed */
}
}
</style>
<script>
// Global filter state
let currentSort = 'month';
let currentRegion = 'vietnam';
function toggleFilterMenu() {
document.getElementById('filterMenu').classList.toggle('show');
}
// Close menu when clicking outside
document.addEventListener('click', function (e) {
const menu = document.getElementById('filterMenu');
const btn = document.getElementById('filterToggleBtn');
if (menu && btn && !menu.contains(e.target) && !btn.contains(e.target)) {
menu.classList.remove('show');
}
});
function changeSort(sort) {
window.currentSort = sort;
// Global loadTrending from main.js will use this
loadTrending(true);
loadShorts(); // Also reload shorts with new sort
toggleFilterMenu();
}
function changeRegion(region) {
window.currentRegion = region;
loadTrending(true);
loadShorts(); // Also reload shorts with new region
toggleFilterMenu();
}
// Scroll shorts logic
function scrollShorts(direction) {
const grid = document.getElementById('shortsGrid');
const scrollAmount = 400;
if (direction === 'left') {
grid.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
} else {
grid.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
}
// Load Shorts Logic
// Load Shorts Logic
async function loadShorts() {
const shortsGrid = document.getElementById('shortsGrid');
// Skeleton loader
shortsGrid.innerHTML = Array(10).fill(0).map(() => `
<div class="skeleton-short"></div>
`).join('');
try {
const page = 1;
const category = window.currentCategory || 'general';
const shortsCategory = category === 'all' || category === 'general' ? 'shorts' : category;
const response = await fetch(`/api/trending?category=${shortsCategory}&page=${page}&sort=${window.currentSort}&region=${window.currentRegion}&shorts=1`);
const data = await response.json();
shortsGrid.innerHTML = '';
if (data && data.length > 0) {
// Show up to 20 shorts
data.slice(0, 20).forEach(video => {
const card = document.createElement('div');
card.className = 'yt-short-card';
card.innerHTML = `
<img data-src="${video.thumbnail}" class="yt-short-thumb">
<p class="yt-short-title">${escapeHtml(video.title)}</p>
<p class="yt-short-views">${formatViews(video.view_count)} views</p>
`;
card.onclick = () => window.location.href = `/watch?v=${video.id}`;
shortsGrid.appendChild(card);
});
if (window.observeImages) window.observeImages();
} else {
shortsGrid.innerHTML = '<p style="color:var(--yt-text-secondary);padding:20px;">No shorts found</p>';
}
} catch (e) {
console.error('Error loading shorts:', e);
shortsGrid.innerHTML = '<p style="color:var(--yt-text-secondary);padding:20px;">Could not load shorts</p>';
}
}
// Helpers (if main.js not loaded yet or for standalone usage)
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatViews(views) {
if (!views) return '0';
const num = parseInt(views);
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(0) + 'K';
return num.toLocaleString();
}
// Init Logic
document.addEventListener('DOMContentLoaded', () => {
loadShorts();
// Pagination logic removed for infinite scroll
// Check URL params for category
const urlParams = new URLSearchParams(window.location.search);
const category = urlParams.get('category');
if (category && typeof switchCategory === 'function') {
// Let main.js handle the switch, but we can set UI active state if needed
// switchCategory is in main.js
switchCategory(category);
}
});
</script>
{% endblock %}