feat: enforce horizontal videos, personalize categories, remove shorts
This commit is contained in:
parent
0ebea200b0
commit
7dee541640
19 changed files with 2601 additions and 2581 deletions
19
README.md
19
README.md
|
|
@ -1,17 +1,22 @@
|
||||||
# KV-Tube
|
#
|
||||||
|
**KV-Tube** is a distraction-free, privacy-focused YouTube frontend designed for a premium viewing experience.
|
||||||
|
|
||||||
A modern, ad-free YouTube web client and video proxy designed for **Synology NAS** and personal home servers.
|
### 🚀 **New Features (v2.0 Updates)**
|
||||||
|
* **Horizontal-First Experience**: Strictly enforces horizontal videos across all categories. "Shorts" and vertical content are aggressively filtered out for a cleaner, cinematic feed.
|
||||||
|
* **Personalized Discovery**:
|
||||||
|
* **Suggested for You**: Dynamic recommendations based on your local watch history.
|
||||||
|
* **You Might Like**: curated discovery topics to help you find new interests.
|
||||||
|
* **Refined Tech Feed**: Specialized "Tech & AI" section focusing on gadget reviews, unboxings, and deep dives (no spammy vertical clips).
|
||||||
|
* **Performance**: Optimized fetching limits to ensure rich, full grids of content despite strict filtering.
|
||||||
|
|
||||||
## ✨ Features
|
## Features
|
||||||
|
* **No Ads**: Watch videos without interruptions.
|
||||||
- **Ad-Free Watching**: Clean interface without distractions.
|
* **Privacy Focused**: No Google account required. Watch history is stored locally (managed by SQLite).
|
||||||
- **Smart Search**: Directly search YouTube content.
|
|
||||||
- **Trending**: Browse trending videos by category (Tech, Music, Gaming, etc.).
|
- **Trending**: Browse trending videos by category (Tech, Music, Gaming, etc.).
|
||||||
- **Auto-Captions**: English subtitles automatically enabled if available.
|
- **Auto-Captions**: English subtitles automatically enabled if available.
|
||||||
- **AI Summary**: (Optional) Extractive summarization of video content running locally.
|
- **AI Summary**: (Optional) Extractive summarization of video content running locally.
|
||||||
- **PWA Ready**: Installable on mobile devices with a responsive drawer layout.
|
- **PWA Ready**: Installable on mobile devices with a responsive drawer layout.
|
||||||
- **Dark/Light Mode**: User preference persisted in settings.
|
- **Dark/Light Mode**: User preference persisted in settings.
|
||||||
- **Privacy Focused**: Everything runs on your server.
|
|
||||||
|
|
||||||
## 🚀 Deployment
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
|
|
||||||
133
app.py
133
app.py
|
|
@ -916,6 +916,11 @@ def summarize_video():
|
||||||
# Helper function to fetch videos (not a route)
|
# Helper function to fetch videos (not a route)
|
||||||
def fetch_videos(query, limit=20, filter_type=None, playlist_start=1, playlist_end=None):
|
def fetch_videos(query, limit=20, filter_type=None, playlist_start=1, playlist_end=None):
|
||||||
try:
|
try:
|
||||||
|
# Source-Level Filter: Exclude Shorts for standard video requests
|
||||||
|
# REMOVED: Causing 0 results with complex queries. Rely on Python filtering.
|
||||||
|
# if filter_type == 'video':
|
||||||
|
# query = f"{query} -shorts -#shorts"
|
||||||
|
|
||||||
# If no end specified, default to start + limit - 1
|
# If no end specified, default to start + limit - 1
|
||||||
if not playlist_end:
|
if not playlist_end:
|
||||||
playlist_end = playlist_start + limit - 1
|
playlist_end = playlist_start + limit - 1
|
||||||
|
|
@ -944,8 +949,19 @@ def fetch_videos(query, limit=20, filter_type=None, playlist_start=1, playlist_e
|
||||||
duration_secs = data.get('duration')
|
duration_secs = data.get('duration')
|
||||||
|
|
||||||
# Filter Logic
|
# Filter Logic
|
||||||
if filter_type == 'video' and duration_secs and int(duration_secs) <= 60:
|
title_lower = data.get('title', '').lower()
|
||||||
|
if filter_type == 'video':
|
||||||
|
# STRICT: If duration is missing, DO NOT SKIP. Just trust the query exclusion.
|
||||||
|
# if not duration_secs:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# Exclude explicit Shorts
|
||||||
|
if '#shorts' in title_lower:
|
||||||
continue
|
continue
|
||||||
|
# Exclude short duration (buffer to 70s to avoid vertical clutter) ONLY IF WE KNOW IT
|
||||||
|
if duration_secs and int(duration_secs) <= 70:
|
||||||
|
continue
|
||||||
|
|
||||||
if filter_type == 'short' and duration_secs and int(duration_secs) > 60:
|
if filter_type == 'short' and duration_secs and int(duration_secs) > 60:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -985,37 +1001,36 @@ def trending():
|
||||||
region = request.args.get('region', 'vietnam')
|
region = request.args.get('region', 'vietnam')
|
||||||
limit = 120 if category != 'all' else 20 # 120 for grid, 20 for sections
|
limit = 120 if category != 'all' else 20 # 120 for grid, 20 for sections
|
||||||
|
|
||||||
# Helper to build query
|
|
||||||
def get_query(cat, reg, s_sort):
|
def get_query(cat, reg, s_sort):
|
||||||
if reg == 'vietnam':
|
if reg == 'vietnam':
|
||||||
queries = {
|
queries = {
|
||||||
'general': 'trending vietnam',
|
'general': 'trending vietnam -shorts',
|
||||||
'tech': 'AI tools software tech review IT việt nam',
|
'tech': 'review công nghệ điện thoại laptop',
|
||||||
'all': 'trending vietnam',
|
'all': 'trending vietnam -shorts',
|
||||||
'music': 'nhạc việt trending',
|
'music': 'nhạc việt trending -shorts',
|
||||||
'gaming': 'gaming việt nam',
|
'gaming': 'gaming việt nam -shorts',
|
||||||
'movies': 'phim việt nam',
|
'movies': 'phim việt nam -shorts',
|
||||||
'news': 'tin tức việt nam hôm nay',
|
'news': 'tin tức việt nam hôm nay -shorts',
|
||||||
'sports': 'thể thao việt nam',
|
'sports': 'thể thao việt nam -shorts',
|
||||||
'shorts': 'trending việt nam',
|
'shorts': 'trending việt nam',
|
||||||
'trending': 'trending việt nam',
|
'trending': 'trending việt nam -shorts',
|
||||||
'podcasts': 'podcast việt nam',
|
'podcasts': 'podcast việt nam -shorts',
|
||||||
'live': 'live stream việt nam'
|
'live': 'live stream việt nam -shorts'
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
queries = {
|
queries = {
|
||||||
'general': 'trending',
|
'general': 'trending -shorts',
|
||||||
'tech': 'AI tools software tech review IT',
|
'tech': 'tech gadget review smartphone',
|
||||||
'all': 'trending',
|
'all': 'trending -shorts',
|
||||||
'music': 'music trending',
|
'music': 'music trending -shorts',
|
||||||
'gaming': 'gaming trending',
|
'gaming': 'gaming trending -shorts',
|
||||||
'movies': 'movies trending',
|
'movies': 'movies trending -shorts',
|
||||||
'news': 'news today',
|
'news': 'news today -shorts',
|
||||||
'sports': 'sports highlights',
|
'sports': 'sports highlights -shorts',
|
||||||
'shorts': 'trending',
|
'shorts': 'trending',
|
||||||
'trending': 'trending now',
|
'trending': 'trending now -shorts',
|
||||||
'podcasts': 'podcast trending',
|
'podcasts': 'podcast trending -shorts',
|
||||||
'live': 'live stream'
|
'live': 'live stream -shorts'
|
||||||
}
|
}
|
||||||
|
|
||||||
base = queries.get(cat, 'trending')
|
base = queries.get(cat, 'trending')
|
||||||
|
|
@ -1039,14 +1054,46 @@ def trending():
|
||||||
|
|
||||||
# === Parallel Fetching for Home Feed ===
|
# === Parallel Fetching for Home Feed ===
|
||||||
if category == 'all':
|
if category == 'all':
|
||||||
|
# === 1. Suggested For You (History Based) ===
|
||||||
|
suggested_videos = []
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
# Get last 5 videos for context
|
||||||
|
history = conn.execute('SELECT title, video_id, type FROM user_videos WHERE type = "history" ORDER BY timestamp DESC LIMIT 5').fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if history:
|
||||||
|
# Create a composite query from history
|
||||||
|
import random
|
||||||
|
# Pick 1-2 random items from recent history to diversify
|
||||||
|
bases = random.sample(history, min(len(history), 2))
|
||||||
|
query_parts = [row['title'] for row in bases]
|
||||||
|
# Add "related" to find similar content, not exact same
|
||||||
|
suggestion_query = " ".join(query_parts) + " related"
|
||||||
|
suggested_videos = fetch_videos(suggestion_query, limit=16, filter_type='video')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Suggestion Error: {e}")
|
||||||
|
|
||||||
|
# === 2. You Might Like (Discovery) ===
|
||||||
|
discovery_videos = []
|
||||||
|
try:
|
||||||
|
# curated list of interesting topics to rotate
|
||||||
|
topics = ['amazing inventions', 'primitive technology', 'street food around the world',
|
||||||
|
'documentary 2024', 'space exploration', 'wildlife 4k', 'satisfying restoration',
|
||||||
|
'travel vlog 4k', 'tech gadgets review', 'coding tutorial']
|
||||||
|
import random
|
||||||
|
topic = random.choice(topics)
|
||||||
|
discovery_videos = fetch_videos(f"{topic} best", limit=16, filter_type='video')
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# === Define Standard Sections ===
|
||||||
sections_to_fetch = [
|
sections_to_fetch = [
|
||||||
{'id': 'trending', 'title': 'Trending Now', 'icon': 'fire'},
|
{'id': 'trending', 'title': 'Trending Now', 'icon': 'fire'},
|
||||||
{'id': 'all', 'title': 'New Releases', 'icon': 'clock'}, # 'all' results in trending, but we'll sort by newest
|
|
||||||
{'id': 'tech', 'title': 'AI & Tech', 'icon': 'microchip'},
|
|
||||||
{'id': 'music', 'title': 'Music', 'icon': 'music'},
|
{'id': 'music', 'title': 'Music', 'icon': 'music'},
|
||||||
|
{'id': 'tech', 'title': 'Tech & AI', 'icon': 'microchip'},
|
||||||
{'id': 'movies', 'title': 'Movies', 'icon': 'film'},
|
{'id': 'movies', 'title': 'Movies', 'icon': 'film'},
|
||||||
{'id': 'news', 'title': 'News', 'icon': 'newspaper'},
|
|
||||||
{'id': 'gaming', 'title': 'Gaming', 'icon': 'gamepad'},
|
{'id': 'gaming', 'title': 'Gaming', 'icon': 'gamepad'},
|
||||||
|
{'id': 'news', 'title': 'News', 'icon': 'newspaper'},
|
||||||
{'id': 'sports', 'title': 'Sports', 'icon': 'football-ball'}
|
{'id': 'sports', 'title': 'Sports', 'icon': 'football-ball'}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1055,7 +1102,9 @@ def trending():
|
||||||
q = get_query(section['id'], region, target_sort)
|
q = get_query(section['id'], region, target_sort)
|
||||||
# Add a unique component to query for freshness
|
# Add a unique component to query for freshness
|
||||||
q_fresh = f"{q} {int(time.time())}" if section['id'] == 'all' else q
|
q_fresh = f"{q} {int(time.time())}" if section['id'] == 'all' else q
|
||||||
vids = fetch_videos(q_fresh, limit=20, filter_type='video', playlist_start=1)
|
|
||||||
|
# Increase fetch limit to 150 (was 100) to compensate for strict filtering (dropping shorts/no-duration)
|
||||||
|
vids = fetch_videos(q_fresh, limit=150, filter_type='video', playlist_start=1)
|
||||||
return {
|
return {
|
||||||
'id': section['id'],
|
'id': section['id'],
|
||||||
'title': section['title'],
|
'title': section['title'],
|
||||||
|
|
@ -1064,9 +1113,33 @@ def trending():
|
||||||
}
|
}
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
|
||||||
results = list(executor.map(fetch_section, sections_to_fetch))
|
standard_results = list(executor.map(fetch_section, sections_to_fetch))
|
||||||
|
|
||||||
return jsonify({'mode': 'sections', 'data': results})
|
# === Assemble Final Feed ===
|
||||||
|
final_sections = []
|
||||||
|
|
||||||
|
# Add Suggested if we have them
|
||||||
|
if suggested_videos:
|
||||||
|
final_sections.append({
|
||||||
|
'id': 'suggested',
|
||||||
|
'title': 'Suggested for You',
|
||||||
|
'icon': 'sparkles',
|
||||||
|
'videos': suggested_videos
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add Discovery
|
||||||
|
if discovery_videos:
|
||||||
|
final_sections.append({
|
||||||
|
'id': 'discovery',
|
||||||
|
'title': 'You Might Like',
|
||||||
|
'icon': 'compass',
|
||||||
|
'videos': discovery_videos
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add Standard Sections
|
||||||
|
final_sections.extend(standard_results)
|
||||||
|
|
||||||
|
return jsonify({'mode': 'sections', 'data': final_sections})
|
||||||
|
|
||||||
# === Standard Single Category Fetch ===
|
# === Standard Single Category Fetch ===
|
||||||
query = get_query(category, region, sort)
|
query = get_query(category, region, sort)
|
||||||
|
|
|
||||||
56
static/css/modules/base.css
Normal file
56
static/css/modules/base.css
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* ===== Reset & Base ===== */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--yt-bg-primary);
|
||||||
|
/* Fix white bar issue */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', 'Arial', sans-serif;
|
||||||
|
background-color: var(--yt-bg-primary);
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar globally but allow scroll */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
325
static/css/modules/cards.css
Normal file
325
static/css/modules/cards.css
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
/* ===== Video Card (Standard) ===== */
|
||||||
|
.yt-video-card {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
/* Animation from style.css */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stagger animation */
|
||||||
|
.yt-video-card:nth-child(1) {
|
||||||
|
animation-delay: 0.05s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:nth-child(2) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:nth-child(3) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:nth-child(4) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:nth-child(5) {
|
||||||
|
animation-delay: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:nth-child(6) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-thumbnail-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-thumbnail.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card:hover .yt-thumbnail {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-duration {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: var(--yt-radius-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-channel-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-channel-name:hover {
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Shorts Card & Container ===== */
|
||||||
|
.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-section-title-link:hover {
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Horizontal Video Card ===== */
|
||||||
|
.yt-video-card-horizontal {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--yt-radius-md);
|
||||||
|
transition: background 0.2s;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-card-horizontal:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-thumb-container-h {
|
||||||
|
position: relative;
|
||||||
|
width: 140px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: var(--yt-radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-thumb-container-h img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-details-h {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-title-h {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-meta-h {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yt-video-card {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 4px !important;
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-thumbnail-container {
|
||||||
|
border-radius: 6px !important;
|
||||||
|
/* V4 Override */
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-details {
|
||||||
|
padding: 6px 8px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-title {
|
||||||
|
font-size: 13px !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-shorts-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
512
static/css/modules/components.css
Normal file
512
static/css/modules/components.css
Normal file
|
|
@ -0,0 +1,512 @@
|
||||||
|
/* ===== Components ===== */
|
||||||
|
|
||||||
|
/* --- Buttons --- */
|
||||||
|
.yt-menu-btn,
|
||||||
|
.yt-icon-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-menu-btn:hover,
|
||||||
|
.yt-icon-btn:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-back-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Button */
|
||||||
|
.yt-search-btn {
|
||||||
|
width: 64px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border: 1px solid var(--yt-border);
|
||||||
|
border-radius: 0 20px 20px 0;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-search-btn:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sign In Button */
|
||||||
|
.yt-signin-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--yt-border);
|
||||||
|
border-radius: var(--yt-radius-pill);
|
||||||
|
color: var(--yt-accent-blue);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-signin-btn:hover {
|
||||||
|
background: rgba(62, 166, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Button */
|
||||||
|
.yt-btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--yt-accent-blue);
|
||||||
|
color: var(--yt-bg-primary);
|
||||||
|
border-radius: var(--yt-radius-md);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Back Button */
|
||||||
|
.yt-floating-back {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: var(--yt-accent-blue);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: none;
|
||||||
|
/* Hidden on desktop */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 2000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, background 0.2s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-floating-back:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
background: #2c95dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yt-floating-back {
|
||||||
|
display: flex;
|
||||||
|
/* Show only on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-floating-back {
|
||||||
|
background: var(--yt-accent-red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-floating-back:active {
|
||||||
|
background: #cc0000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Inputs --- */
|
||||||
|
.yt-search-form {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-search-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border: 1px solid var(--yt-border);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 20px 0 0 20px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-search-input:focus {
|
||||||
|
border-color: var(--yt-accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-search-input::placeholder {
|
||||||
|
color: var(--yt-text-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--yt-bg-primary);
|
||||||
|
border: 1px solid var(--yt-border);
|
||||||
|
border-radius: var(--yt-radius-md);
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-form-input:focus {
|
||||||
|
border-color: var(--yt-accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yt-search-input {
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 18px 0 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-search-btn {
|
||||||
|
width: 48px;
|
||||||
|
border-radius: 0 18px 18px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Search Bar */
|
||||||
|
.yt-mobile-search-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--yt-header-height);
|
||||||
|
background: var(--yt-bg-primary);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 12px;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-mobile-search-bar.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-mobile-search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-mobile-search {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Avatars --- */
|
||||||
|
.yt-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--yt-accent-blue);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-channel-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-channel-avatar-lg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Categories / Pills --- */
|
||||||
|
.yt-categories {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0 24px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE/Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-categories::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-chip,
|
||||||
|
.yt-category-pill {
|
||||||
|
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: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-category-pill {
|
||||||
|
padding: 8px 12px;
|
||||||
|
/* style.css match */
|
||||||
|
border-radius: var(--yt-radius-pill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-chip:hover,
|
||||||
|
.yt-category-pill:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-chip-active,
|
||||||
|
.yt-category-pill.active {
|
||||||
|
background: var(--yt-text-primary);
|
||||||
|
color: var(--yt-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-chip-active:hover {
|
||||||
|
background: var(--yt-text-primary);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yt-categories {
|
||||||
|
padding: 8px 0 8px 8px !important;
|
||||||
|
gap: 8px;
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
width: 100% !important;
|
||||||
|
mask-image: linear-gradient(to right, black 95%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to right, black 95%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-chip,
|
||||||
|
.yt-category-pill {
|
||||||
|
font-size: 12px !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
height: 30px !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Dropdowns --- */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Queue Drawer --- */
|
||||||
|
.yt-queue-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -350px;
|
||||||
|
width: 350px;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
z-index: 10000;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-drawer.open {
|
||||||
|
right: 0;
|
||||||
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--yt-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-header h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-footer {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid var(--yt-border);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-clear-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--yt-border);
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-clear-btn:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-thumb {
|
||||||
|
width: 100px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-info {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-title:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-uploader {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-queue-remove:hover {
|
||||||
|
color: #ff4e45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.yt-queue-drawer {
|
||||||
|
width: 85%;
|
||||||
|
right: -85%;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
static/css/modules/grid.css
Normal file
96
static/css/modules/grid.css
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/* ===== Video Grid ===== */
|
||||||
|
.yt-video-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
|
||||||
|
.yt-video-grid,
|
||||||
|
.yt-section-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
|
||||||
|
.yt-video-grid,
|
||||||
|
.yt-section-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
|
.yt-video-grid,
|
||||||
|
.yt-section-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid Layout for Sections (4 rows x 4 columns = 16 videos) */
|
||||||
|
.yt-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-section-grid .yt-video-card {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar hiding */
|
||||||
|
.yt-section-grid::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Grid Overrides */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
/* Main Grid */
|
||||||
|
.yt-video-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
padding: 0 4px !important;
|
||||||
|
background: var(--yt-bg-primary);
|
||||||
|
gap: 1px !important;
|
||||||
|
/* Minimal gap from V4 override */
|
||||||
|
padding: 0 !important;
|
||||||
|
/* V4 override */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Grid (Horizontal Carousel) */
|
||||||
|
.yt-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: none;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-auto-columns: 85%;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-section-grid::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust video card size for single row */
|
||||||
|
.yt-section-grid .yt-video-card {
|
||||||
|
width: 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet Grid */
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.yt-video-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
251
static/css/modules/layout.css
Normal file
251
static/css/modules/layout.css
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
/* ===== App Layout ===== */
|
||||||
|
.app-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Header (YouTube Style) ===== */
|
||||||
|
.yt-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--yt-header-height);
|
||||||
|
background: var(--yt-bg-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
border-bottom: 1px solid var(--yt-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-header-start {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-header-center {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 728px;
|
||||||
|
margin: 0 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-header-end {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 200px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
.yt-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-logo-icon {
|
||||||
|
width: 90px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--yt-accent-red);
|
||||||
|
border-radius: var(--yt-radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar (YouTube Style) ===== */
|
||||||
|
.yt-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--yt-header-height);
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--yt-sidebar-width);
|
||||||
|
background: var(--yt-bg-primary);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 12px 0;
|
||||||
|
z-index: 900;
|
||||||
|
transition: width 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar.collapsed {
|
||||||
|
width: var(--yt-sidebar-mini);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 10px 12px 10px 24px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
margin: 0 12px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-item:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-item.active {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-item i {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 22px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-item span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar.collapsed .yt-sidebar-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 16px 0;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar.collapsed .yt-sidebar-item span {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--yt-border);
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-title {
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Overlay (Mobile) */
|
||||||
|
.yt-sidebar-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--yt-header-height);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 899;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar-overlay.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Main Content ===== */
|
||||||
|
.yt-main {
|
||||||
|
margin-top: var(--yt-header-height);
|
||||||
|
margin-left: var(--yt-sidebar-width);
|
||||||
|
padding: 24px;
|
||||||
|
min-height: calc(100vh - var(--yt-header-height));
|
||||||
|
transition: margin-left 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-main.sidebar-collapsed {
|
||||||
|
margin-left: var(--yt-sidebar-mini);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Filter Bar ===== */
|
||||||
|
/* From index.html originally */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Responsive Layout Overrides ===== */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.yt-sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-main {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-header-center {
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yt-header-center {
|
||||||
|
display: flex;
|
||||||
|
/* Show search on mobile */
|
||||||
|
margin: 0 8px;
|
||||||
|
max-width: none;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-header-start,
|
||||||
|
.yt-header-end {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-logo span:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-main {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce header padding to match */
|
||||||
|
.yt-header {
|
||||||
|
padding: 0 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter bar spacing */
|
||||||
|
.yt-filter-bar {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.yt-main {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
249
static/css/modules/pages.css
Normal file
249
static/css/modules/pages.css
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
/* ===== Watch Page ===== */
|
||||||
|
.yt-watch-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
gap: 24px;
|
||||||
|
max-width: 1800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-player-section {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-player-container {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: #000;
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-info {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-info h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-video-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border-radius: var(--yt-radius-pill);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-action-btn:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-action-btn.active {
|
||||||
|
background: var(--yt-text-primary);
|
||||||
|
color: var(--yt-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-channel-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid var(--yt-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-channel-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-subscribe-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--yt-text-primary);
|
||||||
|
color: var(--yt-bg-primary);
|
||||||
|
border-radius: var(--yt-radius-pill);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-subscribe-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-subscribe-btn.subscribed {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-description-box {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-description-box:hover {
|
||||||
|
background: var(--yt-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-description-stats {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggested Videos */
|
||||||
|
.yt-suggested {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 80px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for suggested videos */
|
||||||
|
.yt-suggested::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--yt-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: var(--yt-radius-md);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-card:hover {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-thumb {
|
||||||
|
width: 168px;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: var(--yt-radius-md);
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-channel {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.yt-watch-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
max-height: none;
|
||||||
|
/* Allow full height on mobile/tablet */
|
||||||
|
position: static;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-suggested-thumb {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Auth Pages ===== */
|
||||||
|
.yt-auth-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: calc(100vh - var(--yt-header-height) - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-auth-card {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
padding: 48px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-auth-card h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-auth-card p {
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
212
static/css/modules/utils.css
Normal file
212
static/css/modules/utils.css
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
/* ===== Animations ===== */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Skeleton Loader (Shimmer) ===== */
|
||||||
|
.skeleton {
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--yt-bg-secondary) 25%,
|
||||||
|
var(--yt-bg-hover) 50%,
|
||||||
|
var(--yt-bg-secondary) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-thumb {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: var(--yt-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-title {
|
||||||
|
height: 14px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-meta {
|
||||||
|
height: 12px;
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-short {
|
||||||
|
width: 180px;
|
||||||
|
height: 320px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--yt-bg-secondary) 25%,
|
||||||
|
var(--yt-bg-hover) 50%,
|
||||||
|
var(--yt-bg-secondary) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-comment {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-comment-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-comment-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line {
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--yt-bg-secondary);
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--yt-bg-secondary) 25%,
|
||||||
|
var(--yt-bg-hover) 50%,
|
||||||
|
var(--yt-bg-secondary) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Loader ===== */
|
||||||
|
.yt-loader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Friendly Empty State ===== */
|
||||||
|
.yt-empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--yt-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-empty-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--yt-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-empty-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Toasts ===== */
|
||||||
|
.yt-toast-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 24px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-toast {
|
||||||
|
background: #1f1f1f;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 14px;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 280px;
|
||||||
|
border-left: 4px solid #3ea6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-toast.error {
|
||||||
|
border-left-color: #ff4e45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yt-toast.success {
|
||||||
|
border-left-color: #2ba640;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
static/css/modules/variables.css
Normal file
43
static/css/modules/variables.css
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* ===== YouTube Dark Theme Colors ===== */
|
||||||
|
:root {
|
||||||
|
--yt-bg-primary: #0f0f0f;
|
||||||
|
--yt-bg-secondary: #333333;
|
||||||
|
--yt-bg-elevated: #282828;
|
||||||
|
--yt-bg-hover: #444444;
|
||||||
|
--yt-bg-active: #3ea6ff;
|
||||||
|
|
||||||
|
--yt-text-primary: #f1f1f1;
|
||||||
|
--yt-text-secondary: #aaaaaa;
|
||||||
|
--yt-text-disabled: #717171;
|
||||||
|
--yt-static-white: #ffffff;
|
||||||
|
|
||||||
|
--yt-accent-red: #ff0000;
|
||||||
|
--yt-accent-blue: #3ea6ff;
|
||||||
|
|
||||||
|
--yt-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--yt-divider: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
--yt-header-height: 56px;
|
||||||
|
--yt-sidebar-width: 240px;
|
||||||
|
--yt-sidebar-mini: 72px;
|
||||||
|
|
||||||
|
--yt-radius-sm: 4px;
|
||||||
|
--yt-radius-md: 8px;
|
||||||
|
--yt-radius-lg: 12px;
|
||||||
|
--yt-radius-xl: 16px;
|
||||||
|
--yt-radius-pill: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--yt-bg-primary: #ffffff;
|
||||||
|
--yt-bg-secondary: #f2f2f2;
|
||||||
|
--yt-bg-elevated: #e5e5e5;
|
||||||
|
--yt-bg-hover: #e5e5e5;
|
||||||
|
|
||||||
|
--yt-text-primary: #0f0f0f;
|
||||||
|
--yt-text-secondary: #606060;
|
||||||
|
--yt-text-disabled: #909090;
|
||||||
|
|
||||||
|
--yt-border: rgba(0, 0, 0, 0.1);
|
||||||
|
--yt-divider: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
1418
static/css/style.css
1418
static/css/style.css
File diff suppressed because it is too large
Load diff
|
|
@ -282,9 +282,28 @@ async function loadTrending(reset = true) {
|
||||||
sectionDiv.style.marginBottom = '24px';
|
sectionDiv.style.marginBottom = '24px';
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
|
// Make title clickable - user request
|
||||||
|
const categoryLink = section.id === 'suggested' || section.id === 'discovery'
|
||||||
|
? '#'
|
||||||
|
: `/?category=${section.id}`;
|
||||||
|
|
||||||
|
// If it is suggested or discovery, maybe we don't link or link to something generic?
|
||||||
|
// User asked for "categories name has a hyperlink".
|
||||||
|
// Standard categories link to their pages. Suggested/Discovery link to # (no-op) or trending?
|
||||||
|
// Let's link standard ones. For Suggested/Discovery, we can just not link or link to home.
|
||||||
|
// Actually, if we link to /?category=tech it works.
|
||||||
|
// Use a conditional logic for href.
|
||||||
|
|
||||||
|
const titleHtml = (section.id !== 'suggested' && section.id !== 'discovery')
|
||||||
|
? `<a href="/?category=${section.id}" class="yt-section-title-link" style="text-decoration:none; color:inherit; display:flex; align-items:center; gap:10px;">
|
||||||
|
<i class="fas fa-${section.icon}"></i> ${section.title}
|
||||||
|
<i class="fas fa-chevron-right" style="font-size: 14px; opacity: 0.7;"></i>
|
||||||
|
</a>`
|
||||||
|
: `<span style="display:flex; align-items:center; gap:10px;"><i class="fas fa-${section.icon}"></i> ${section.title}</span>`;
|
||||||
|
|
||||||
sectionDiv.innerHTML = `
|
sectionDiv.innerHTML = `
|
||||||
<div class="yt-section-header" style="margin-bottom:12px;">
|
<div class="yt-section-header" style="margin-bottom:12px;">
|
||||||
<h2><i class="fas fa-${section.icon}"></i> ${section.title}</h2>
|
<h2>${titleHtml}</h2>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@
|
||||||
<div class="yt-filter-bar">
|
<div class="yt-filter-bar">
|
||||||
<div class="yt-categories" id="categoryList">
|
<div class="yt-categories" id="categoryList">
|
||||||
<!-- Pinned Categories -->
|
<!-- Pinned Categories -->
|
||||||
<button class="yt-chip" onclick="switchCategory('history', this)"><i class="fas fa-history"></i> Watched</button>
|
<button class="yt-chip" onclick="switchCategory('history', this)"><i class="fas fa-history"></i>
|
||||||
<button class="yt-chip" onclick="switchCategory('suggested', this)"><i class="fas fa-magic"></i> Suggested</button>
|
Watched</button>
|
||||||
|
<button class="yt-chip" onclick="switchCategory('suggested', this)"><i class="fas fa-magic"></i>
|
||||||
|
Suggested</button>
|
||||||
<!-- Standard Categories -->
|
<!-- Standard Categories -->
|
||||||
<button class="yt-chip" onclick="switchCategory('tech', this)">Tech</button>
|
<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('music', this)">Music</button>
|
||||||
|
|
@ -145,251 +147,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<!-- Styles moved to CSS modules -->
|
||||||
/* 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>
|
<script>
|
||||||
// Global filter state
|
// Global filter state
|
||||||
|
|
@ -413,71 +171,15 @@
|
||||||
window.currentSort = sort;
|
window.currentSort = sort;
|
||||||
// Global loadTrending from main.js will use this
|
// Global loadTrending from main.js will use this
|
||||||
loadTrending(true);
|
loadTrending(true);
|
||||||
loadShorts(); // Also reload shorts with new sort
|
|
||||||
toggleFilterMenu();
|
toggleFilterMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeRegion(region) {
|
function changeRegion(region) {
|
||||||
window.currentRegion = region;
|
window.currentRegion = region;
|
||||||
loadTrending(true);
|
loadTrending(true);
|
||||||
loadShorts(); // Also reload shorts with new region
|
|
||||||
toggleFilterMenu();
|
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}®ion=${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)
|
// Helpers (if main.js not loaded yet or for standalone usage)
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
|
|
@ -496,7 +198,6 @@
|
||||||
|
|
||||||
// Init Logic
|
// Init Logic
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadShorts();
|
|
||||||
// Pagination logic removed for infinite scroll
|
// Pagination logic removed for infinite scroll
|
||||||
|
|
||||||
// Check URL params for category
|
// Check URL params for category
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,6 @@
|
||||||
<i class="fas fa-home"></i>
|
<i class="fas fa-home"></i>
|
||||||
<span>Home</span>
|
<span>Home</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:void(0)" class="yt-sidebar-item" data-category="shorts"
|
|
||||||
onclick="navigateCategory('shorts')">
|
|
||||||
<i class="fas fa-bolt"></i>
|
|
||||||
<span>Shorts</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="yt-sidebar-divider"></div>
|
<div class="yt-sidebar-divider"></div>
|
||||||
|
|
||||||
|
|
@ -300,54 +295,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="yt-queue-overlay" id="queueOverlay" onclick="toggleQueue()"></div>
|
<div class="yt-queue-overlay" id="queueOverlay" onclick="toggleQueue()"></div>
|
||||||
|
|
||||||
<style>
|
<!-- Toast Styles Moved to static/css/modules/utils.css -->
|
||||||
.yt-toast-container {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
left: 24px;
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-toast {
|
|
||||||
background: #1f1f1f;
|
|
||||||
color: #fff;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
font-size: 14px;
|
|
||||||
animation: slideUp 0.3s ease;
|
|
||||||
pointer-events: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
min-width: 280px;
|
|
||||||
border-left: 4px solid #3ea6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-toast.error {
|
|
||||||
border-left-color: #ff4e45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-toast.success {
|
|
||||||
border-left-color: #2ba640;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
|
|
@ -424,148 +372,6 @@
|
||||||
// --- Back Button Logic ---
|
// --- Back Button Logic ---
|
||||||
// Back Button Logic Removed (Handled Server-Side)
|
// Back Button Logic Removed (Handled Server-Side)
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<!-- Queue Drawer Styles Moved to static/css/modules/components.css -->
|
||||||
/* Queue Drawer Styles */
|
|
||||||
.yt-queue-drawer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: -350px;
|
|
||||||
width: 350px;
|
|
||||||
height: 100vh;
|
|
||||||
background: var(--yt-bg-secondary);
|
|
||||||
z-index: 10000;
|
|
||||||
transition: right 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
/* Shadow removed here to prevent bleeding when closed */
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-drawer.open {
|
|
||||||
right: 0;
|
|
||||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
|
|
||||||
/* Apply shadow only when open */
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-header {
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid var(--yt-border);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-header h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-footer {
|
|
||||||
padding: 16px;
|
|
||||||
border-top: 1px solid var(--yt-border);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-clear-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--yt-border);
|
|
||||||
color: var(--yt-text-primary);
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-clear-btn:hover {
|
|
||||||
background: var(--yt-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-thumb {
|
|
||||||
width: 100px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-thumb img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-info {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-title:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-uploader {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--yt-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-remove {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--yt-text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-remove:hover {
|
|
||||||
color: #ff4e45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 9999;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yt-queue-overlay.active {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.yt-queue-drawer {
|
|
||||||
width: 85%;
|
|
||||||
right: -85%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
66
test_crawl.py
Normal file
66
test_crawl.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
def test_fetch(query, label):
|
||||||
|
print(f"--- Testing Query: '{query}' ({label}) ---")
|
||||||
|
limit = 150
|
||||||
|
cmd = [
|
||||||
|
sys.executable, '-m', 'yt_dlp',
|
||||||
|
f'ytsearch{limit}:{query}',
|
||||||
|
'--dump-json',
|
||||||
|
'--default-search', 'ytsearch',
|
||||||
|
'--no-playlist',
|
||||||
|
'--flat-playlist',
|
||||||
|
'--playlist-start', '1',
|
||||||
|
'--playlist-end', str(limit)
|
||||||
|
]
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
stdout, stderr = process.communicate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error running command: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_count = 0
|
||||||
|
valid_count = 0
|
||||||
|
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
raw_count += 1
|
||||||
|
|
||||||
|
# Simulate our strict filter filters
|
||||||
|
video_id = data.get('id')
|
||||||
|
title = data.get('title', '').lower()
|
||||||
|
duration_secs = data.get('duration')
|
||||||
|
|
||||||
|
if not video_id: continue
|
||||||
|
|
||||||
|
# Title Filter
|
||||||
|
if '#shorts' in title:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Duration Filter (Loose: allow missing, strict if present)
|
||||||
|
if duration_secs and int(duration_secs) <= 70:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are here, it's valid
|
||||||
|
valid_count += 1
|
||||||
|
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"Raw Results: {raw_count}")
|
||||||
|
print(f"Valid Horizontal Videos (filtered): {valid_count}")
|
||||||
|
print(f"Time Taken: {elapsed:.2f}s")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test queries from app.py
|
||||||
|
test_fetch("review công nghệ điện thoại laptop", "Vietnam Tech")
|
||||||
|
test_fetch("tech gadget review smartphone", "Global Tech")
|
||||||
Loading…
Reference in a new issue