diff --git a/README.md b/README.md index 507111b..d9c0f9e 100644 --- a/README.md +++ b/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 - -- **Ad-Free Watching**: Clean interface without distractions. -- **Smart Search**: Directly search YouTube content. +## Features +* **No Ads**: Watch videos without interruptions. +* **Privacy Focused**: No Google account required. Watch history is stored locally (managed by SQLite). - **Trending**: Browse trending videos by category (Tech, Music, Gaming, etc.). - **Auto-Captions**: English subtitles automatically enabled if available. - **AI Summary**: (Optional) Extractive summarization of video content running locally. - **PWA Ready**: Installable on mobile devices with a responsive drawer layout. - **Dark/Light Mode**: User preference persisted in settings. -- **Privacy Focused**: Everything runs on your server. ## 🚀 Deployment diff --git a/app.py b/app.py index 41c6388..01bbe71 100644 --- a/app.py +++ b/app.py @@ -916,6 +916,11 @@ def summarize_video(): # Helper function to fetch videos (not a route) def fetch_videos(query, limit=20, filter_type=None, playlist_start=1, playlist_end=None): 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 not playlist_end: 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') # Filter Logic - if filter_type == 'video' and duration_secs and int(duration_secs) <= 60: - continue + 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 + # 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: continue @@ -985,37 +1001,36 @@ def trending(): region = request.args.get('region', 'vietnam') limit = 120 if category != 'all' else 20 # 120 for grid, 20 for sections - # Helper to build query def get_query(cat, reg, s_sort): if reg == 'vietnam': queries = { - 'general': 'trending vietnam', - 'tech': 'AI tools software tech review IT việt nam', - 'all': 'trending vietnam', - 'music': 'nhạc việt trending', - 'gaming': 'gaming việt nam', - 'movies': 'phim việt nam', - 'news': 'tin tức việt nam hôm nay', - 'sports': 'thể thao việt nam', - 'shorts': 'trending việt nam', - 'trending': 'trending việt nam', - 'podcasts': 'podcast việt nam', - 'live': 'live stream việt nam' + 'general': 'trending vietnam -shorts', + 'tech': 'review công nghệ điện thoại laptop', + 'all': 'trending vietnam -shorts', + 'music': 'nhạc việt trending -shorts', + 'gaming': 'gaming việt nam -shorts', + 'movies': 'phim việt nam -shorts', + 'news': 'tin tức việt nam hôm nay -shorts', + 'sports': 'thể thao việt nam -shorts', + 'shorts': 'trending việt nam', + 'trending': 'trending việt nam -shorts', + 'podcasts': 'podcast việt nam -shorts', + 'live': 'live stream việt nam -shorts' } else: queries = { - 'general': 'trending', - 'tech': 'AI tools software tech review IT', - 'all': 'trending', - 'music': 'music trending', - 'gaming': 'gaming trending', - 'movies': 'movies trending', - 'news': 'news today', - 'sports': 'sports highlights', + 'general': 'trending -shorts', + 'tech': 'tech gadget review smartphone', + 'all': 'trending -shorts', + 'music': 'music trending -shorts', + 'gaming': 'gaming trending -shorts', + 'movies': 'movies trending -shorts', + 'news': 'news today -shorts', + 'sports': 'sports highlights -shorts', 'shorts': 'trending', - 'trending': 'trending now', - 'podcasts': 'podcast trending', - 'live': 'live stream' + 'trending': 'trending now -shorts', + 'podcasts': 'podcast trending -shorts', + 'live': 'live stream -shorts' } base = queries.get(cat, 'trending') @@ -1039,14 +1054,46 @@ def trending(): # === Parallel Fetching for Home Feed === 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 = [ {'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': 'tech', 'title': 'Tech & AI', 'icon': 'microchip'}, {'id': 'movies', 'title': 'Movies', 'icon': 'film'}, - {'id': 'news', 'title': 'News', 'icon': 'newspaper'}, {'id': 'gaming', 'title': 'Gaming', 'icon': 'gamepad'}, + {'id': 'news', 'title': 'News', 'icon': 'newspaper'}, {'id': 'sports', 'title': 'Sports', 'icon': 'football-ball'} ] @@ -1055,7 +1102,9 @@ def trending(): q = get_query(section['id'], region, target_sort) # Add a unique component to query for freshness 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 { 'id': section['id'], 'title': section['title'], @@ -1064,9 +1113,33 @@ def trending(): } 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 === query = get_query(category, region, sort) diff --git a/deploy-docker.bat b/deploy-docker.bat index d618619..954379f 100644 --- a/deploy-docker.bat +++ b/deploy-docker.bat @@ -1,66 +1,66 @@ -@echo off -REM deploy-docker.bat - Build and push KV-Tube to Docker Hub - -set DOCKER_USER=vndangkhoa -set IMAGE_NAME=kvtube -set TAG=latest -set FULL_IMAGE=%DOCKER_USER%/%IMAGE_NAME%:%TAG% - -echo ======================================== -echo KV-Tube Docker Deployment Script -echo ======================================== -echo. - -REM Step 1: Check Docker -echo [1/4] Checking Docker... -docker info >nul 2>&1 -if %errorlevel% neq 0 ( - echo X Docker is not running. Please start Docker Desktop. - pause - exit /b 1 -) -echo OK Docker is running - -REM Step 2: Build Image -echo. -echo [2/4] Building Docker image: %FULL_IMAGE% -docker build --no-cache -t %FULL_IMAGE% . -if %errorlevel% neq 0 ( - echo X Build failed! - pause - exit /b 1 -) -echo OK Build successful - -REM Step 3: Login to Docker Hub -echo. -echo [3/4] Logging into Docker Hub... -docker login -if %errorlevel% neq 0 ( - echo X Login failed! - pause - exit /b 1 -) -echo OK Login successful - -REM Step 4: Push Image -echo. -echo [4/4] Pushing to Docker Hub... -docker push %FULL_IMAGE% -if %errorlevel% neq 0 ( - echo X Push failed! - pause - exit /b 1 -) -echo OK Push successful - -echo. -echo ======================================== -echo Deployment Complete! -echo Image: %FULL_IMAGE% -echo URL: https://hub.docker.com/r/%DOCKER_USER%/%IMAGE_NAME% -echo ======================================== -echo. -echo To run: docker run -p 5001:5001 %FULL_IMAGE% -echo. -pause +@echo off +REM deploy-docker.bat - Build and push KV-Tube to Docker Hub + +set DOCKER_USER=vndangkhoa +set IMAGE_NAME=kvtube +set TAG=latest +set FULL_IMAGE=%DOCKER_USER%/%IMAGE_NAME%:%TAG% + +echo ======================================== +echo KV-Tube Docker Deployment Script +echo ======================================== +echo. + +REM Step 1: Check Docker +echo [1/4] Checking Docker... +docker info >nul 2>&1 +if %errorlevel% neq 0 ( + echo X Docker is not running. Please start Docker Desktop. + pause + exit /b 1 +) +echo OK Docker is running + +REM Step 2: Build Image +echo. +echo [2/4] Building Docker image: %FULL_IMAGE% +docker build --no-cache -t %FULL_IMAGE% . +if %errorlevel% neq 0 ( + echo X Build failed! + pause + exit /b 1 +) +echo OK Build successful + +REM Step 3: Login to Docker Hub +echo. +echo [3/4] Logging into Docker Hub... +docker login +if %errorlevel% neq 0 ( + echo X Login failed! + pause + exit /b 1 +) +echo OK Login successful + +REM Step 4: Push Image +echo. +echo [4/4] Pushing to Docker Hub... +docker push %FULL_IMAGE% +if %errorlevel% neq 0 ( + echo X Push failed! + pause + exit /b 1 +) +echo OK Push successful + +echo. +echo ======================================== +echo Deployment Complete! +echo Image: %FULL_IMAGE% +echo URL: https://hub.docker.com/r/%DOCKER_USER%/%IMAGE_NAME% +echo ======================================== +echo. +echo To run: docker run -p 5001:5001 %FULL_IMAGE% +echo. +pause diff --git a/deploy-docker.ps1 b/deploy-docker.ps1 index 944593d..038fd55 100644 --- a/deploy-docker.ps1 +++ b/deploy-docker.ps1 @@ -1,63 +1,63 @@ -#!/usr/bin/env pwsh -# deploy-docker.ps1 - Build and push KV-Tube to Docker Hub - -$ErrorActionPreference = "Stop" - -$DOCKER_USER = "vndangkhoa" -$IMAGE_NAME = "kvtube" -$TAG = "latest" -$FULL_IMAGE = "${DOCKER_USER}/${IMAGE_NAME}:${TAG}" - -Write-Host "========================================" -ForegroundColor Cyan -Write-Host " KV-Tube Docker Deployment Script" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" - -# Step 1: Check Docker -Write-Host "[1/4] Checking Docker..." -ForegroundColor Yellow -try { - docker info | Out-Null - Write-Host " ✓ Docker is running" -ForegroundColor Green -} catch { - Write-Host " ✗ Docker is not running. Please start Docker Desktop." -ForegroundColor Red - exit 1 -} - -# Step 2: Build Image -Write-Host "" -Write-Host "[2/4] Building Docker image: $FULL_IMAGE" -ForegroundColor Yellow -docker build --no-cache -t $FULL_IMAGE . -if ($LASTEXITCODE -ne 0) { - Write-Host " ✗ Build failed!" -ForegroundColor Red - exit 1 -} -Write-Host " ✓ Build successful" -ForegroundColor Green - -# Step 3: Login to Docker Hub -Write-Host "" -Write-Host "[3/4] Logging into Docker Hub..." -ForegroundColor Yellow -docker login -if ($LASTEXITCODE -ne 0) { - Write-Host " ✗ Login failed!" -ForegroundColor Red - exit 1 -} -Write-Host " ✓ Login successful" -ForegroundColor Green - -# Step 4: Push Image -Write-Host "" -Write-Host "[4/4] Pushing to Docker Hub..." -ForegroundColor Yellow -docker push $FULL_IMAGE -if ($LASTEXITCODE -ne 0) { - Write-Host " ✗ Push failed!" -ForegroundColor Red - exit 1 -} -Write-Host " ✓ Push successful" -ForegroundColor Green - -Write-Host "" -Write-Host "========================================" -ForegroundColor Cyan -Write-Host " Deployment Complete!" -ForegroundColor Cyan -Write-Host " Image: $FULL_IMAGE" -ForegroundColor Cyan -Write-Host " URL: https://hub.docker.com/r/${DOCKER_USER}/${IMAGE_NAME}" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" -Write-Host "To run: docker run -p 5001:5001 $FULL_IMAGE" -ForegroundColor White +#!/usr/bin/env pwsh +# deploy-docker.ps1 - Build and push KV-Tube to Docker Hub + +$ErrorActionPreference = "Stop" + +$DOCKER_USER = "vndangkhoa" +$IMAGE_NAME = "kvtube" +$TAG = "latest" +$FULL_IMAGE = "${DOCKER_USER}/${IMAGE_NAME}:${TAG}" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " KV-Tube Docker Deployment Script" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +# Step 1: Check Docker +Write-Host "[1/4] Checking Docker..." -ForegroundColor Yellow +try { + docker info | Out-Null + Write-Host " ✓ Docker is running" -ForegroundColor Green +} catch { + Write-Host " ✗ Docker is not running. Please start Docker Desktop." -ForegroundColor Red + exit 1 +} + +# Step 2: Build Image +Write-Host "" +Write-Host "[2/4] Building Docker image: $FULL_IMAGE" -ForegroundColor Yellow +docker build --no-cache -t $FULL_IMAGE . +if ($LASTEXITCODE -ne 0) { + Write-Host " ✗ Build failed!" -ForegroundColor Red + exit 1 +} +Write-Host " ✓ Build successful" -ForegroundColor Green + +# Step 3: Login to Docker Hub +Write-Host "" +Write-Host "[3/4] Logging into Docker Hub..." -ForegroundColor Yellow +docker login +if ($LASTEXITCODE -ne 0) { + Write-Host " ✗ Login failed!" -ForegroundColor Red + exit 1 +} +Write-Host " ✓ Login successful" -ForegroundColor Green + +# Step 4: Push Image +Write-Host "" +Write-Host "[4/4] Pushing to Docker Hub..." -ForegroundColor Yellow +docker push $FULL_IMAGE +if ($LASTEXITCODE -ne 0) { + Write-Host " ✗ Push failed!" -ForegroundColor Red + exit 1 +} +Write-Host " ✓ Push successful" -ForegroundColor Green + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Deployment Complete!" -ForegroundColor Cyan +Write-Host " Image: $FULL_IMAGE" -ForegroundColor Cyan +Write-Host " URL: https://hub.docker.com/r/${DOCKER_USER}/${IMAGE_NAME}" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "To run: docker run -p 5001:5001 $FULL_IMAGE" -ForegroundColor White diff --git a/doc/Product Requirements Document (PRD) - KV-Tube b/doc/Product Requirements Document (PRD) - KV-Tube index 5d5cd69..9c03269 100644 --- a/doc/Product Requirements Document (PRD) - KV-Tube +++ b/doc/Product Requirements Document (PRD) - KV-Tube @@ -1,46 +1,46 @@ -Product Requirements Document (PRD) - KV-Tube -1. Product Overview -Product Name: KV-Tube Version: 1.0 (In Development) Description: KV-Tube is a comprehensive media center web application designed to provide an ad-free YouTube experience, a curated movie streaming service, and a local video management system. It emphasizes privacy, absence of advertisements, and utility features like AI summarization and language learning tools. - -2. User Personas -The Binge Watcher: Wants uninterrupted access to YouTube content and movies without ads. -The Archivist: Maintains a local collection of videos and wants a clean interface to organize and watch them securely. -The Learner: Uses video content for educational purposes, specifically English learning. -The Tech Enthusiast: Appreciates PWA support, torrent integration, and customizable settings. -3. Core Features -3.1. YouTube Viewer (Home) -Ad-Free Experience: Plays YouTube videos without third-party advertisements. -Search: Integrated search bar powered by yt-dlp to find videos, channels, and playlists. -Playback: Custom video player with support for quality selection and playback speed. -AI Summarization: Feature to summarize video content using Google Gemini API (Optional). -3.2. local Video Manager ("My Videos") -Secure Access: Password-protected section for personal video collections. -File Management: Scans local directories for video files. -Metadata: Extracts metadata (duration, format) and generates thumbnails using FFmpeg/MoviePy. -Playback: Native HTML5 player for local files. -3.3. Utilities -Torrent Player: Interface for streaming/playing video content via torrents. -Playlist Manager: Create and manage custom playlists of YouTube videos. -Camera/Photo: ("Chụp ảnh") Feature to capture or manage photos (Webcam integration). -Configuration: Web-based settings to manage application behavior (e.g., password, storage paths). -4. Technical Architecture -Backend: Python / Flask -Frontend: HTML5, CSS3, JavaScript (Vanilla) -Database/Storage: JSON-based local storage and file system. -Video Processing: yt-dlp (YouTube), FFmpeg (Conversion/Thumbnail), MoviePy (Optional). -AI Service: Google Gemini API (for summarization). -Deployment: Docker container support (xehopnet/kctube). -5. Non-Functional Requirements -Performance: Fast load times and responsive UI. -Compatibility: PWA-ready for installation on desktop and mobile. -Reliability: graceful degradation if optional dependencies (MoviePy, Gemini) are missing. -Privacy: No user tracking or external analytics. -6. Known Limitations -Search Reliability: Dependent on yt-dlp stability and YouTube's anti-bot measures. -External APIs: Movie features rely on third-party APIs which may have downtime. -Dependency Management: Some Python libraries (MoviePy, Numpy) require compilation tools. -7. Future Roadmap -Database: Migrate from JSON to SQLite for better performance with large libraries. -User Accounts: Individual user profiles and history. -Offline Mode: Enhanced offline capabilities for PWA. +Product Requirements Document (PRD) - KV-Tube +1. Product Overview +Product Name: KV-Tube Version: 1.0 (In Development) Description: KV-Tube is a comprehensive media center web application designed to provide an ad-free YouTube experience, a curated movie streaming service, and a local video management system. It emphasizes privacy, absence of advertisements, and utility features like AI summarization and language learning tools. + +2. User Personas +The Binge Watcher: Wants uninterrupted access to YouTube content and movies without ads. +The Archivist: Maintains a local collection of videos and wants a clean interface to organize and watch them securely. +The Learner: Uses video content for educational purposes, specifically English learning. +The Tech Enthusiast: Appreciates PWA support, torrent integration, and customizable settings. +3. Core Features +3.1. YouTube Viewer (Home) +Ad-Free Experience: Plays YouTube videos without third-party advertisements. +Search: Integrated search bar powered by yt-dlp to find videos, channels, and playlists. +Playback: Custom video player with support for quality selection and playback speed. +AI Summarization: Feature to summarize video content using Google Gemini API (Optional). +3.2. local Video Manager ("My Videos") +Secure Access: Password-protected section for personal video collections. +File Management: Scans local directories for video files. +Metadata: Extracts metadata (duration, format) and generates thumbnails using FFmpeg/MoviePy. +Playback: Native HTML5 player for local files. +3.3. Utilities +Torrent Player: Interface for streaming/playing video content via torrents. +Playlist Manager: Create and manage custom playlists of YouTube videos. +Camera/Photo: ("Chụp ảnh") Feature to capture or manage photos (Webcam integration). +Configuration: Web-based settings to manage application behavior (e.g., password, storage paths). +4. Technical Architecture +Backend: Python / Flask +Frontend: HTML5, CSS3, JavaScript (Vanilla) +Database/Storage: JSON-based local storage and file system. +Video Processing: yt-dlp (YouTube), FFmpeg (Conversion/Thumbnail), MoviePy (Optional). +AI Service: Google Gemini API (for summarization). +Deployment: Docker container support (xehopnet/kctube). +5. Non-Functional Requirements +Performance: Fast load times and responsive UI. +Compatibility: PWA-ready for installation on desktop and mobile. +Reliability: graceful degradation if optional dependencies (MoviePy, Gemini) are missing. +Privacy: No user tracking or external analytics. +6. Known Limitations +Search Reliability: Dependent on yt-dlp stability and YouTube's anti-bot measures. +External APIs: Movie features rely on third-party APIs which may have downtime. +Dependency Management: Some Python libraries (MoviePy, Numpy) require compilation tools. +7. Future Roadmap +Database: Migrate from JSON to SQLite for better performance with large libraries. +User Accounts: Individual user profiles and history. +Offline Mode: Enhanced offline capabilities for PWA. Casting: Support for Chromecast/AirPlay. \ No newline at end of file diff --git a/static/css/modules/base.css b/static/css/modules/base.css new file mode 100644 index 0000000..d0451c2 --- /dev/null +++ b/static/css/modules/base.css @@ -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); +} \ No newline at end of file diff --git a/static/css/modules/cards.css b/static/css/modules/cards.css new file mode 100644 index 0000000..1c5e729 --- /dev/null +++ b/static/css/modules/cards.css @@ -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; + } +} \ No newline at end of file diff --git a/static/css/modules/components.css b/static/css/modules/components.css new file mode 100644 index 0000000..cbca29d --- /dev/null +++ b/static/css/modules/components.css @@ -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%; + } +} \ No newline at end of file diff --git a/static/css/modules/grid.css b/static/css/modules/grid.css new file mode 100644 index 0000000..15f614f --- /dev/null +++ b/static/css/modules/grid.css @@ -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); + } +} \ No newline at end of file diff --git a/static/css/modules/layout.css b/static/css/modules/layout.css new file mode 100644 index 0000000..fbe9375 --- /dev/null +++ b/static/css/modules/layout.css @@ -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; + } +} \ No newline at end of file diff --git a/static/css/modules/pages.css b/static/css/modules/pages.css new file mode 100644 index 0000000..774890a --- /dev/null +++ b/static/css/modules/pages.css @@ -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; +} \ No newline at end of file diff --git a/static/css/modules/utils.css b/static/css/modules/utils.css new file mode 100644 index 0000000..50bc869 --- /dev/null +++ b/static/css/modules/utils.css @@ -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; + } +} \ No newline at end of file diff --git a/static/css/modules/variables.css b/static/css/modules/variables.css new file mode 100644 index 0000000..27bf11d --- /dev/null +++ b/static/css/modules/variables.css @@ -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); +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index b13f965..4e1eb9b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,1411 +1,17 @@ /* KV-Tube - YouTube Clone Design System */ -/* ===== 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; +/* Core */ +@import 'modules/variables.css'; +@import 'modules/base.css'; +@import 'modules/utils.css'; - --yt-text-primary: #f1f1f1; - --yt-text-secondary: #aaaaaa; - --yt-text-disabled: #717171; - --yt-static-white: #ffffff; +/* Layout & Structure */ +@import 'modules/layout.css'; +@import 'modules/grid.css'; - --yt-accent-red: #ff0000; - --yt-accent-blue: #3ea6ff; +/* Components */ +@import 'modules/components.css'; +@import 'modules/cards.css'; - --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-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); -} - -/* ===== 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); -} - -/* ===== 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; -} - -/* Menu Button */ -.yt-menu-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; -} - -.yt-menu-btn:hover { - background: var(--yt-bg-hover); -} - -/* Logo */ -.yt-logo { - display: flex; - align-items: center; - gap: 4px; - font-size: 20px; - font-weight: 600; - color: var(--yt-text-primary); -} - -.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; -} - -/* Search Bar */ -.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-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); -} - -/* Header Icons */ -.yt-icon-btn { - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: var(--yt-text-primary); - font-size: 20px; - transition: background 0.2s; -} - -.yt-icon-btn:hover { - background: var(--yt-bg-hover); -} - -/* Avatar */ -.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-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); -} - -/* ===== 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; -} - -/* ===== 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); -} - -/* ===== Category Pills ===== */ -.yt-categories { - display: flex; - gap: 12px; - padding: 12px 0 24px; - overflow-x: auto; - scrollbar-width: none; - flex-wrap: nowrap; - -ms-overflow-style: none; -} - -.yt-categories::-webkit-scrollbar { - display: none; -} - -.yt-category-pill { - padding: 8px 12px; - background: var(--yt-bg-secondary); - border-radius: var(--yt-radius-pill); - font-size: 14px; - font-weight: 500; - white-space: nowrap; - color: var(--yt-text-primary); - transition: background 0.2s; -} - -.yt-category-pill:hover { - background: var(--yt-bg-hover); -} - -.yt-category-pill.active { - background: var(--yt-text-primary); - color: var(--yt-bg-primary); -} - -/* ===== 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; -} - -@media (max-width: 768px) { - .yt-section-grid { - grid-template-rows: 1fr; - /* Single row per container */ - grid-auto-columns: 80%; - /* Peek effect */ - gap: 12px; - } - - /* Adjust video card size for single row */ - .yt-section-grid .yt-video-card { - width: 100%; - /* Fill the column width */ - } -} - -.yt-section-grid::-webkit-scrollbar { - display: none; -} - -/* ===== Video Card (YouTube Style) ===== */ -.yt-video-card { - cursor: pointer; - border-radius: var(--yt-radius-lg); - overflow: hidden; - transition: transform 0.1s; -} - -.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-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-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); -} - -/* ===== 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-subscribe-btn.subscribed { - background: var(--yt-bg-secondary); - color: var(--yt-text-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-channel-avatar-lg { - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--yt-bg-secondary); -} - -.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-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); -} - -/* ===== 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; -} - -.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); -} - -.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; -} - -/* ===== Loader ===== */ -/* ===== Loader (SOTA Quantum Style) ===== */ -.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; -} - -/* Spinner removed */ - -/* ===== Responsive ===== */ -@media (max-width: 1200px) { - .yt-watch-layout { - grid-template-columns: 1fr; - } - - .yt-suggested { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - } - - .yt-suggested-card { - flex-direction: column; - } - - .yt-suggested-thumb { - width: 100%; - } -} - -@media (max-width: 1024px) { - .yt-sidebar { - transform: translateX(-100%); - } - - .yt-sidebar.open { - transform: translateX(0); - } - - .yt-main { - margin-left: 0; - } -} - -@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-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; - } - - .yt-mobile-search { - display: none; - /* Hide icon since bar is visible */ - } - - .yt-signin-text { - display: none; - } - - .yt-video-grid { - grid-template-columns: repeat(2, 1fr); - gap: 8px; - padding: 0 4px; - } - - .yt-video-card { - border-radius: 0; - padding: 12px; - } - - .yt-thumbnail-container { - border-radius: 12px; - } - - .yt-main { - padding: 12px; - } - - .yt-categories { - padding: 8px 0 16px; - gap: 8px; - display: flex; - flex-wrap: nowrap; - overflow-x: auto; - white-space: nowrap; - } - - .yt-category-pill { - padding: 6px 10px; - font-size: 13px; - } -} - -/* Tablet */ -@media (min-width: 769px) and (max-width: 1024px) { - .yt-video-grid { - grid-template-columns: repeat(2, 1fr); - } - - .yt-main { - padding: 16px; - } -} - -/* Mobile Search Bar */ -.yt-mobile-search { - display: none; -} - -.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-back-btn { - width: 40px; - height: 40px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: var(--yt-text-primary); -} - -/* 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; -} - -/* ===== Animations ===== */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.yt-video-card { - animation: fadeIn 0.3s ease forwards; -} - -/* Stagger animation for grid */ -.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; -} - -/* ===== 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; -} - -/* 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; -} - -/* ===== Horizontal Video Card (Suggested/Sidebar) ===== */ -.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; -} - -/* 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 */ - } -} - -/* Mobile V2 Overrides */ -@media (max-width: 768px) { - .yt-video-grid { - grid-template-columns: repeat(2, 1fr) !important; - gap: 8px !important; - padding: 0 4px !important; - } - - .yt-video-card { - padding: 4px !important; - } - - .yt-categories { - display: flex !important; - flex-wrap: nowrap !important; - grid-template-rows: none !important; - padding-right: 16px !important; - white-space: nowrap !important; - } - - .yt-floating-back { - background: var(--yt-accent-red) !important; - } - - .yt-floating-back:active { - background: #cc0000 !important; - } -} - -/* Mobile V4 Overrides - Pixel Perfect Polish */ -@media (max-width: 768px) { - - /* Optimize Categories */ - .yt-categories { - padding: 8px 0 8px 8px !important; - /* Left padding for start, no right padding */ - 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 { - font-size: 12px !important; - padding: 6px 12px !important; - height: 30px !important; - border-radius: 6px !important; - } - - /* Maximize Thumbnails */ - .yt-main { - padding: 0 !important; - /* Edge to edge */ - } - - .yt-video-grid { - gap: 1px !important; - /* Minimal gap */ - padding: 0 !important; - background: var(--yt-bg-primary); - } - - .yt-video-card, - .skeleton-card { - padding: 0 !important; - margin-bottom: 4px !important; - } - - .yt-thumbnail-container, - .skeleton-thumb { - border-radius: 6px !important; - } - - .yt-video-details { - padding: 6px 8px 12px !important; - } - - .yt-video-title { - font-size: 13px !important; - line-height: 1.2 !important; - } - - /* 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; - } -} \ No newline at end of file +/* Pages */ +@import 'modules/pages.css'; \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 0851eda..5c54c16 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -282,11 +282,30 @@ async function loadTrending(reset = true) { sectionDiv.style.marginBottom = '24px'; // 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') + ? ` + ${section.title} + + ` + : ` ${section.title}`; + sectionDiv.innerHTML = `