From 60d3bc3a5e1717bbbba53379c9c31622b423d317 Mon Sep 17 00:00:00 2001
From: KV-Tube Deployer
Date: Sun, 11 Jan 2026 21:51:42 +0700
Subject: [PATCH] Fix subscribe button and library tab navigation
---
.dockerignore | 12 +
deploy_v2.ps1 | 27 +++
static/css/modules/layout.css | 25 +-
static/css/modules/pages.css | 2 +-
static/css/modules/watch.css | 105 ++++++++-
static/js/main.js | 28 ++-
templates/channel.html | 431 ++++++++++++++++++----------------
templates/layout.html | 6 -
templates/my_videos.html | 277 +++++++++++++++++++---
templates/watch.html | 177 +++++++++++++-
wsgi.py | 273 +++++++++++++++------
11 files changed, 1034 insertions(+), 329 deletions(-)
create mode 100644 .dockerignore
create mode 100644 deploy_v2.ps1
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..83b1373
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+__pycache__
+.venv
+.git
+.env
+*.mp4
+*.webm
+*.mp3
+videos/
+data/
+temp/
+deployment_package/
+kvtube.db
diff --git a/deploy_v2.ps1 b/deploy_v2.ps1
new file mode 100644
index 0000000..e461323
--- /dev/null
+++ b/deploy_v2.ps1
@@ -0,0 +1,27 @@
+# deploy_v2.ps1 - Deploy KV-Tube v2.0
+
+Write-Host "--- KV-Tube v2.0 Deployment ---" -ForegroundColor Cyan
+
+# 1. Check Git Remote
+Write-Host "1. Pushing to Git..." -ForegroundColor Yellow
+# Note: Ensure 'origin' is the correct writable remote, not a mirror.
+git push -u origin main --tags
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "Error: Git push failed. Verify that 'origin' is not a read-only mirror." -ForegroundColor Red
+ # Continue anyway to try Docker?
+}
+
+# 2. Build Docker Image
+Write-Host "2. Building Docker Image (linux/amd64)..." -ForegroundColor Yellow
+# Requires Docker Desktop to be running
+docker build --platform linux/amd64 -t kv-tube:v2.0 .
+
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "Success! Docker image 'kv-tube:v2.0' built." -ForegroundColor Green
+} else {
+ Write-Host "Error: Docker build failed. Is Docker Desktop running?" -ForegroundColor Red
+}
+
+Write-Host "Done." -ForegroundColor Cyan
+pause
diff --git a/static/css/modules/layout.css b/static/css/modules/layout.css
index 8cc9c33..20f1e81 100644
--- a/static/css/modules/layout.css
+++ b/static/css/modules/layout.css
@@ -120,16 +120,37 @@
.yt-sidebar.collapsed .yt-sidebar-item {
flex-direction: column;
- gap: 6px;
+ gap: 0;
padding: 16px 0;
margin: 0;
border-radius: 0;
justify-content: center;
+ align-items: center;
text-align: center;
}
+/* Hide text labels in collapsed mode - icons only */
.yt-sidebar.collapsed .yt-sidebar-item span {
- font-size: 10px;
+ display: none;
+}
+
+/* Center icons in collapsed mode */
+.yt-sidebar.collapsed .yt-sidebar-item i {
+ font-size: 20px;
+ width: 100%;
+ text-align: center;
+}
+
+/* Hide Saved, Subscriptions, dividers and titles in collapsed mode */
+.yt-sidebar.collapsed .yt-sidebar-title,
+.yt-sidebar.collapsed .yt-sidebar-divider {
+ display: none;
+}
+
+/* Hide Saved and Subscriptions globally (both full and collapsed sidebar) */
+.yt-sidebar a[data-category="saved"],
+.yt-sidebar a[data-category="subscriptions"] {
+ display: none;
}
.yt-sidebar-divider {
diff --git a/static/css/modules/pages.css b/static/css/modules/pages.css
index 774890a..13966c5 100644
--- a/static/css/modules/pages.css
+++ b/static/css/modules/pages.css
@@ -1,9 +1,9 @@
/* ===== Watch Page ===== */
+/* Layout rules moved to watch.css - this is kept for compatibility */
.yt-watch-layout {
display: grid;
grid-template-columns: 1fr 400px;
gap: 24px;
- max-width: 1800px;
}
.yt-player-section {
diff --git a/static/css/modules/watch.css b/static/css/modules/watch.css
index 2db7839..dcb31c7 100644
--- a/static/css/modules/watch.css
+++ b/static/css/modules/watch.css
@@ -90,33 +90,118 @@ body {
}
/* ========== Watch Page Layout ========== */
-.yt-main {
+/* Only apply these overrides when the watch layout is present */
+.yt-main:has(.yt-watch-layout) {
padding: 0 !important;
- margin-left: 240px;
+ /* Auto-collapse main content margin on watch page to match collapsed sidebar */
+ margin-left: var(--yt-sidebar-mini) !important;
}
+/* Auto-collapse sidebar on watch page */
+.yt-sidebar:has(~ .yt-sidebar-overlay ~ .yt-main .yt-watch-layout),
+body:has(.yt-watch-layout) .yt-sidebar {
+ width: var(--yt-sidebar-mini);
+}
+
+/* Sidebar item styling for mini mode on watch page */
+body:has(.yt-watch-layout) .yt-sidebar .yt-sidebar-item {
+ flex-direction: column;
+ gap: 0;
+ padding: 16px 0;
+ margin: 0;
+ border-radius: 0;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+
+/* Hide text labels in mini mode - icons only */
+body:has(.yt-watch-layout) .yt-sidebar .yt-sidebar-item span {
+ display: none;
+}
+
+/* Center the icons */
+body:has(.yt-watch-layout) .yt-sidebar .yt-sidebar-item i {
+ font-size: 20px;
+ width: 100%;
+ text-align: center;
+}
+
+/* Hide Saved, Subscriptions, and dividers/titles on watch page */
+body:has(.yt-watch-layout) .yt-sidebar .yt-sidebar-title,
+body:has(.yt-watch-layout) .yt-sidebar .yt-sidebar-divider,
+body:has(.yt-watch-layout) .yt-sidebar a[data-category="saved"],
+body:has(.yt-watch-layout) .yt-sidebar a[data-category="subscriptions"] {
+ display: none;
+}
+
+/* Theater Mode (Default) - Full width video with sidebar below */
.yt-watch-layout {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ padding: 8px 24px 24px;
+ box-sizing: border-box;
+}
+
+/* Default Mode - 2 column layout */
+.yt-watch-layout.default-mode {
display: grid;
grid-template-columns: 1fr 400px;
gap: 24px;
max-width: 100%;
- width: 100%;
- padding: 24px;
- margin: 0;
- box-sizing: border-box;
+}
+
+.yt-watch-layout.default-mode .yt-watch-sidebar {
+ position: sticky;
+ top: 80px;
+ align-self: start;
+ max-height: calc(100vh - 100px);
+}
+
+/* Theater mode sidebar moves below */
+.yt-watch-layout:not(.default-mode) .yt-watch-sidebar {
+ margin-top: 24px;
}
.yt-watch-sidebar {
display: flex;
flex-direction: column;
gap: 0;
- position: sticky;
- top: 80px;
- align-self: start;
- max-height: calc(100vh - 100px);
overflow: visible;
}
+/* View Mode Button Styles */
+.view-mode-buttons {
+ display: flex;
+ gap: 8px;
+ margin-left: auto;
+}
+
+.view-mode-btn {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: none;
+ background: var(--yt-bg-secondary);
+ color: var(--yt-text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+}
+
+.view-mode-btn:hover {
+ background: var(--yt-bg-hover);
+ color: var(--yt-text-primary);
+}
+
+.view-mode-btn.active {
+ background: linear-gradient(135deg, #cc0000 0%, #ff4444 100%);
+ color: white;
+}
+
.yt-channel-avatar-lg {
width: 40px;
height: 40px;
diff --git a/static/js/main.js b/static/js/main.js
index 371bdc9..21f531c 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -245,7 +245,18 @@ async function switchCategory(category, btn) {
return;
}
if (category === 'suggested') {
- const response = await fetch('/api/suggested');
+ // Build query params from localStorage history
+ const history = JSON.parse(localStorage.getItem('kv_history') || '[]');
+ const titles = history.slice(0, 5).map(v => v.title).filter(Boolean).join(',');
+ const channels = history.slice(0, 3).map(v => v.uploader).filter(Boolean).join(',');
+
+ let url = '/api/suggested';
+ const params = new URLSearchParams();
+ if (titles) params.append('titles', titles);
+ if (channels) params.append('channels', channels);
+ if (params.toString()) url += '?' + params.toString();
+
+ const response = await fetch(url);
const data = await response.json();
displayResults(data, false);
isLoading = false;
@@ -296,7 +307,20 @@ async function loadTrending(reset = true) {
const regionValue = window.currentRegion || 'vietnam';
// Add cache-buster for home page to ensure fresh content
const cb = reset && currentCategory === 'all' ? `&_=${Date.now()}` : '';
- const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}®ion=${regionValue}${cb}`);
+
+ // Include localStorage history for personalized suggestions on home page
+ let historyParams = '';
+ if (currentCategory === 'all') {
+ const history = JSON.parse(localStorage.getItem('kv_history') || '[]');
+ if (history.length > 0) {
+ const titles = history.slice(0, 5).map(v => v.title).filter(Boolean).join(',');
+ const channels = history.slice(0, 3).map(v => v.uploader).filter(Boolean).join(',');
+ if (titles) historyParams += `&history_titles=${encodeURIComponent(titles)}`;
+ if (channels) historyParams += `&history_channels=${encodeURIComponent(channels)}`;
+ }
+ }
+
+ const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}®ion=${regionValue}${historyParams}${cb}`);
const data = await response.json();
diff --git a/templates/channel.html b/templates/channel.html
index 011a106..3a8fd52 100644
--- a/templates/channel.html
+++ b/templates/channel.html
@@ -22,10 +22,7 @@
%}@Loading...{% endif %}
- Subscribe for more
-
-
-
+
@@ -35,14 +32,29 @@
@@ -129,53 +141,58 @@
.yt-tabs {
display: inline-flex;
- gap: 0;
+ gap: 8px;
background: var(--yt-bg-secondary);
- padding: 4px;
- border-radius: 24px;
- position: relative;
+ padding: 6px;
+ border-radius: 16px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
.yt-tabs a {
- font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 24px;
+ border-radius: 12px;
+ font-size: 0.95rem;
font-weight: 600;
color: var(--yt-text-secondary);
text-decoration: none;
- padding: 8px 24px;
- border-radius: 20px;
- border-bottom: none;
- z-index: 1;
- transition: color 0.2s;
- position: relative;
+ transition: all 0.25s ease;
+ border: none;
+ background: transparent;
}
.yt-tabs a:hover {
color: var(--yt-text-primary);
+ background: var(--yt-bg-hover);
}
.yt-tabs a.active {
- color: var(--yt-bg-primary);
- background: var(--yt-text-primary);
- /* The "slider" is actually the active pill moving */
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ background: linear-gradient(135deg, #cc0000 0%, #ff4444 100%);
+ color: white;
+ box-shadow: 0 4px 12px rgba(204, 0, 0, 0.3);
}
.yt-sort-options {
- display: flex;
+ display: inline-flex;
gap: 8px;
- justify-content: center;
+ background: var(--yt-bg-secondary);
+ padding: 6px;
+ border-radius: 16px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
.yt-sort-options a {
- padding: 6px 16px;
- border-radius: 16px;
+ padding: 10px 20px;
+ border-radius: 12px;
background: transparent;
- border: 1px solid var(--yt-border);
+ border: none;
color: var(--yt-text-secondary);
text-decoration: none;
- font-size: 13px;
- font-weight: 500;
- transition: all 0.2s;
+ font-size: 0.9rem;
+ font-weight: 600;
+ transition: all 0.25s ease;
}
.yt-sort-options a:hover {
@@ -184,9 +201,9 @@
}
.yt-sort-options a.active {
- background: var(--yt-bg-secondary);
- color: var(--yt-text-primary);
- border-color: var(--yt-text-primary);
+ background: linear-gradient(135deg, #cc0000 0%, #ff4444 100%);
+ color: white;
+ box-shadow: 0 4px 12px rgba(204, 0, 0, 0.3);
}
/* Shorts Card Styling override for Channel Page grid */
@@ -245,133 +262,134 @@
{% endblock %}
\ No newline at end of file
diff --git a/templates/layout.html b/templates/layout.html
index 748e076..7cc13c5 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -141,12 +141,6 @@
-
-