mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-14 16:23:43 +08:00
feat: Add new animations and optimize global CSS for improved performance and readability.
This commit is contained in:
+109
-58
@@ -3,13 +3,14 @@
|
||||
|
||||
:root {
|
||||
--font-family-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif;
|
||||
|
||||
|
||||
/* Light Mode Palette */
|
||||
--bg-color-light: #f0f2f5;
|
||||
--bg-image-light: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
|
||||
--text-color-light: #1d1d1f;
|
||||
--text-color-secondary-light: #6e6e73;
|
||||
--accent-color-light: #0056b3; /* Updated from #007aff for WCAG AA compliance (6.85:1 with white) */
|
||||
--accent-color-light: #0056b3;
|
||||
/* Updated from #007aff for WCAG AA compliance (6.85:1 with white) */
|
||||
--glass-bg-light: rgba(242, 242, 247, 0.8);
|
||||
--glass-border-light: rgba(255, 255, 255, 0.5);
|
||||
--shadow-color-light: rgba(0, 0, 0, 0.1);
|
||||
@@ -19,18 +20,19 @@
|
||||
--bg-image-dark: linear-gradient(120deg, #272B30 0%, #121212 100%);
|
||||
--text-color-dark: #f5f5f7;
|
||||
--text-color-secondary-dark: #8e8e93;
|
||||
--accent-color-dark: #1A6DBF; /* Updated for WCAG AA compliance (5.27:1 with white) */
|
||||
--accent-color-dark: #1A6DBF;
|
||||
/* Updated for WCAG AA compliance (5.27:1 with white) */
|
||||
--glass-bg-dark: rgba(28, 28, 30, 0.75);
|
||||
--glass-border-dark: rgba(60, 60, 60, 0.7);
|
||||
--shadow-color-dark: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Universal Variables */
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
--shadow-sm: 0 2px 4px var(--shadow-color);
|
||||
--shadow-md: 0 4px 12px var(--shadow-color);
|
||||
--transition-fluid: 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
|
||||
|
||||
/* Active Theme Variables */
|
||||
--bg-color: var(--bg-color-light);
|
||||
--bg-image: var(--bg-image-light);
|
||||
@@ -40,7 +42,7 @@
|
||||
--glass-bg: var(--glass-bg-light);
|
||||
--glass-border: var(--glass-border-light);
|
||||
--shadow-color: var(--shadow-color-light);
|
||||
|
||||
|
||||
--background: #f0f2f5;
|
||||
--foreground: #1d1d1f;
|
||||
}
|
||||
@@ -54,12 +56,12 @@ body {
|
||||
--glass-bg: var(--glass-bg-light);
|
||||
--glass-border: var(--glass-border-light);
|
||||
--shadow-color: var(--shadow-color-light);
|
||||
|
||||
|
||||
background-color: var(--bg-color);
|
||||
background-image: var(--bg-image);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family-system);
|
||||
|
||||
|
||||
/* Optimize scrolling performance */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: none;
|
||||
@@ -75,12 +77,14 @@ body.dark,
|
||||
--glass-bg: var(--glass-bg-dark);
|
||||
--glass-border: var(--glass-border-dark);
|
||||
--shadow-color: var(--shadow-color-dark);
|
||||
|
||||
|
||||
--background: #121212;
|
||||
--foreground: #f5f5f7;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
/* Optimize font rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -164,6 +168,7 @@ select:focus-visible {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -175,6 +180,7 @@ select:focus-visible {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -182,9 +188,12 @@ select:focus-visible {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -197,23 +206,45 @@ select:focus-visible {
|
||||
}
|
||||
|
||||
@keyframes spin-slow {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-reverse {
|
||||
from { transform: rotate(360deg); }
|
||||
to { transform: rotate(0deg); }
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-subtle {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
@@ -221,6 +252,7 @@ select:focus-visible {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
@@ -232,6 +264,7 @@ select:focus-visible {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-10px);
|
||||
@@ -239,10 +272,13 @@ select:focus-visible {
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-20px) translateX(10px);
|
||||
opacity: 0.8;
|
||||
@@ -250,11 +286,14 @@ select:focus-visible {
|
||||
}
|
||||
|
||||
@keyframes gradient-x {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,9 +348,14 @@ select:focus-visible {
|
||||
|
||||
/* Liquid Glass Components */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||
/* background: var(--glass-bg); */
|
||||
/* Performance fix: Use solid color with opacity instead of blur */
|
||||
background: var(--bg-color);
|
||||
opacity: 0.95;
|
||||
|
||||
/* backdrop-filter: blur(8px) saturate(120%); */
|
||||
/* -webkit-backdrop-filter: blur(8px) saturate(120%); */
|
||||
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--glass-border);
|
||||
@@ -333,8 +377,9 @@ nav:where(.sticky, .fixed),
|
||||
|
||||
.glass-input {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(8px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(120%);
|
||||
/* Performance fix: Disable blur */
|
||||
/* backdrop-filter: blur(6px) saturate(120%); */
|
||||
/* -webkit-backdrop-filter: blur(6px) saturate(120%); */
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
color: var(--text-color);
|
||||
@@ -388,6 +433,7 @@ nav:where(.sticky, .fixed),
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
@@ -398,6 +444,7 @@ nav:where(.sticky, .fixed),
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -407,6 +454,7 @@ nav:where(.sticky, .fixed),
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
@@ -508,12 +556,15 @@ nav:where(.sticky, .fixed),
|
||||
|
||||
/* Hide scrollbar utility */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
display: none;
|
||||
/* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
/* Toast slide-up animation */
|
||||
@@ -522,6 +573,7 @@ nav:where(.sticky, .fixed),
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -534,14 +586,14 @@ nav:where(.sticky, .fixed),
|
||||
|
||||
/* Performance optimizations for scrolling */
|
||||
/* Apply CSS containment to grid items for better scroll performance */
|
||||
[class*="grid"] > * {
|
||||
[class*="grid"]>* {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 400px;
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
/* Smooth video grid transitions */
|
||||
[class*="grid"] > a {
|
||||
[class*="grid"]>a {
|
||||
animation: fadeInUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@@ -550,6 +602,7 @@ nav:where(.sticky, .fixed),
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -590,30 +643,30 @@ nav {
|
||||
/* Removed position: fixed - now using absolute from component */
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
|
||||
/* Liquid Glass effect - Core aesthetic */
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(25px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(180%);
|
||||
|
||||
|
||||
/* Rounded-2xl corners - Universal softness */
|
||||
border-radius: var(--radius-2xl);
|
||||
|
||||
|
||||
/* Depth and definition */
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--glass-border);
|
||||
|
||||
|
||||
/* Content padding */
|
||||
padding: 0.75rem;
|
||||
|
||||
|
||||
/* Smooth appearance animation */
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
animation: search-dropdown-appear 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
|
||||
|
||||
/* Transform origin for natural scaling */
|
||||
transform-origin: top center;
|
||||
|
||||
|
||||
/* Prevent text selection during interaction */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -624,6 +677,7 @@ nav {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
@@ -660,16 +714,16 @@ nav {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
|
||||
|
||||
/* Universal rounded-2xl for container elements */
|
||||
border-radius: var(--radius-2xl);
|
||||
|
||||
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
/* Fluid animation - Physics-based transitions */
|
||||
transition: all var(--transition-fluid);
|
||||
|
||||
|
||||
/* Subtle background for hover/highlight */
|
||||
background: transparent;
|
||||
}
|
||||
@@ -684,7 +738,7 @@ nav {
|
||||
.search-history-item.highlighted {
|
||||
background: color-mix(in srgb, var(--accent-color) 15%, transparent);
|
||||
transform: translateX(4px);
|
||||
|
||||
|
||||
/* Subtle inner-glow effect - Lensing principle */
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-color) 30%, transparent);
|
||||
}
|
||||
@@ -694,22 +748,22 @@ nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
/* Rounded-full for small circular elements */
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color-secondary);
|
||||
|
||||
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
/* Fluid animation */
|
||||
transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
|
||||
|
||||
/* Prevent triggering parent hover */
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -777,13 +831,13 @@ nav {
|
||||
/* Increase touch target sizes */
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.search-history-item {
|
||||
padding: 1rem 0.75rem;
|
||||
/* Larger minimum touch target */
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
|
||||
.search-history-remove {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -796,14 +850,11 @@ body.dark .search-history-dropdown,
|
||||
/* Enhanced glass effect in dark mode */
|
||||
backdrop-filter: blur(30px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(200%);
|
||||
|
||||
|
||||
/* Stronger shadow for depth */
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
END SEARCH HISTORY DROPDOWN
|
||||
=========================================== */
|
||||
|
||||
|
||||
|
||||
=========================================== */
|
||||
+16
-17
@@ -85,14 +85,14 @@ export default function SettingsPage() {
|
||||
className="inline-flex items-center gap-2 text-[var(--accent-color)] hover:underline mb-4"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
</svg>
|
||||
返回上一页
|
||||
</button>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 flex items-center justify-center rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)]">
|
||||
<div className="w-12 h-12 flex items-center justify-center rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] backdrop-blur-md border border-[var(--glass-border)]">
|
||||
<svg className="w-6 h-6 text-[var(--text-color)]" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/>
|
||||
<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -103,7 +103,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Source Management */}
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6 mb-6">
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-md border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-color)]">视频源管理</h2>
|
||||
<div className="flex gap-2">
|
||||
@@ -124,9 +124,9 @@ export default function SettingsPage() {
|
||||
<p className="text-sm text-[var(--text-color-secondary)] mb-6">
|
||||
管理视频来源,调整优先级和启用状态
|
||||
</p>
|
||||
<SourceManager
|
||||
sources={showAllSources ? sources : sources.slice(0, 10)}
|
||||
onSourcesChange={handleSourcesChange}
|
||||
<SourceManager
|
||||
sources={showAllSources ? sources : sources.slice(0, 10)}
|
||||
onSourcesChange={handleSourcesChange}
|
||||
/>
|
||||
{sources.length > 10 && (
|
||||
<button
|
||||
@@ -139,7 +139,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Sort Options */}
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6 mb-6">
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-md border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-color)] mb-4">搜索结果排序</h2>
|
||||
<p className="text-sm text-[var(--text-color-secondary)] mb-4">
|
||||
选择搜索结果的默认排序方式
|
||||
@@ -149,11 +149,10 @@ export default function SettingsPage() {
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => handleSortChange(option)}
|
||||
className={`px-4 py-3 rounded-[var(--radius-2xl)] border text-left font-medium transition-all duration-200 ${
|
||||
sortBy === option
|
||||
? 'bg-[var(--accent-color)] border-[var(--accent-color)] text-white'
|
||||
: 'bg-[var(--glass-bg)] border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)]'
|
||||
}`}
|
||||
className={`px-4 py-3 rounded-[var(--radius-2xl)] border text-left font-medium transition-all duration-200 ${sortBy === option
|
||||
? 'bg-[var(--accent-color)] border-[var(--accent-color)] text-white'
|
||||
: 'bg-[var(--glass-bg)] border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)]'
|
||||
}`}
|
||||
>
|
||||
{sortOptions[option]}
|
||||
</button>
|
||||
@@ -162,7 +161,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Data Management */}
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6">
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-md border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-color)] mb-4">数据管理</h2>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
@@ -171,7 +170,7 @@ export default function SettingsPage() {
|
||||
>
|
||||
<span>导出设置</span>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -181,7 +180,7 @@ export default function SettingsPage() {
|
||||
>
|
||||
<span>导入设置</span>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -191,7 +190,7 @@ export default function SettingsPage() {
|
||||
>
|
||||
<span>清除所有数据</span>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function HistoryItem({
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
@@ -47,11 +47,11 @@ export function HistoryItem({
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
|
||||
if (days === 0) return '今天';
|
||||
if (days === 1) return '昨天';
|
||||
if (days < 7) return `${days}天前`;
|
||||
|
||||
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ export function HistoryItem({
|
||||
};
|
||||
|
||||
const progress = (playbackPosition / duration) * 100;
|
||||
const episodeText = episodes && episodes.length > 0
|
||||
const episodeText = episodes && episodes.length > 0
|
||||
? episodes[episodeIndex]?.name || `第${episodeIndex + 1}集`
|
||||
: '';
|
||||
|
||||
@@ -102,6 +102,7 @@ export function HistoryItem({
|
||||
alt={title}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="112px"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
|
||||
@@ -84,7 +84,7 @@ export function WatchHistorySidebar() {
|
||||
{/* Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed right-6 top-1/2 -translate-y-1/2 z-40 bg-[var(--glass-bg)] backdrop-blur-[12px] saturate-[120%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-3 hover:scale-105 transition-transform duration-200"
|
||||
className="fixed right-6 top-1/2 -translate-y-1/2 z-40 bg-[var(--glass-bg)] backdrop-blur-[8px] saturate-[120%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-3 hover:scale-105 transition-transform duration-200"
|
||||
aria-label="打开观看历史"
|
||||
>
|
||||
<Icons.History size={24} className="text-[var(--text-color)]" />
|
||||
@@ -104,17 +104,17 @@ export function WatchHistorySidebar() {
|
||||
role="complementary"
|
||||
aria-labelledby="history-sidebar-title"
|
||||
aria-hidden={!isOpen}
|
||||
style={{
|
||||
style={{
|
||||
transform: isOpen ? 'translate3d(0, 0, 0)' : 'translate3d(100%, 0, 0)',
|
||||
willChange: isOpen ? 'transform' : 'auto'
|
||||
}}
|
||||
className={`fixed top-0 right-0 bottom-0 w-[85%] sm:w-[90%] max-w-[420px] z-[2000] bg-[var(--glass-bg)] backdrop-blur-[12px] saturate-[120%] border-l border-[var(--glass-border)] rounded-tl-[var(--radius-2xl)] rounded-bl-[var(--radius-2xl)] p-6 flex flex-col shadow-[0_8px_32px_rgba(0,0,0,0.2)] transition-transform duration-250 ease-out`}
|
||||
className={`fixed top-0 right-0 bottom-0 w-[85%] sm:w-[90%] max-w-[420px] z-[2000] bg-[var(--glass-bg)] backdrop-blur-[8px] saturate-[120%] border-l border-[var(--glass-border)] rounded-tl-[var(--radius-2xl)] rounded-bl-[var(--radius-2xl)] p-6 flex flex-col shadow-[0_8px_32px_rgba(0,0,0,0.2)] transition-transform duration-250 ease-out`}
|
||||
>
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between mb-6 pb-4 border-b border-[var(--glass-border)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icons.History size={24} className="text-[var(--accent-color)]" />
|
||||
<h2
|
||||
<h2
|
||||
id="history-sidebar-title"
|
||||
className="text-xl font-semibold text-[var(--text-color)]"
|
||||
>
|
||||
@@ -131,7 +131,7 @@ export function WatchHistorySidebar() {
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto -mx-2 px-2" style={{
|
||||
<div className="flex-1 overflow-y-auto -mx-2 px-2" style={{
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
WebkitOverflowScrolling: 'touch'
|
||||
}}>
|
||||
|
||||
@@ -32,7 +32,7 @@ export const MovieCard = memo(function MovieCard({ movie, onMovieClick }: MovieC
|
||||
}}
|
||||
className="group cursor-pointer"
|
||||
>
|
||||
<Card hover className="overflow-hidden p-0 h-full">
|
||||
<Card hover className="overflow-hidden p-0 h-full" blur={false}>
|
||||
<div className="relative aspect-[2/3] overflow-hidden bg-[var(--glass-bg)] rounded-[var(--radius-2xl)]">
|
||||
<Image
|
||||
src={movie.cover}
|
||||
@@ -55,7 +55,7 @@ export const MovieCard = memo(function MovieCard({ movie, onMovieClick }: MovieC
|
||||
}}
|
||||
/>
|
||||
{movie.rate && parseFloat(movie.rate) > 0 && (
|
||||
<div
|
||||
<div
|
||||
className="absolute top-2 right-2 bg-black/80 px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)]"
|
||||
>
|
||||
<Icons.Star size={12} className="text-yellow-400 fill-yellow-400" />
|
||||
|
||||
@@ -27,28 +27,28 @@ interface VideoGridProps {
|
||||
}
|
||||
|
||||
// Memoized VideoCard component to prevent unnecessary re-renders
|
||||
const VideoCard = memo(({
|
||||
video,
|
||||
videoUrl,
|
||||
cardId,
|
||||
isActive,
|
||||
onCardClick
|
||||
}: {
|
||||
video: Video;
|
||||
videoUrl: string;
|
||||
cardId: string;
|
||||
isActive: boolean;
|
||||
const VideoCard = memo(({
|
||||
video,
|
||||
videoUrl,
|
||||
cardId,
|
||||
isActive,
|
||||
onCardClick
|
||||
}: {
|
||||
video: Video;
|
||||
videoUrl: string;
|
||||
cardId: string;
|
||||
isActive: boolean;
|
||||
onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
style={{
|
||||
// CSS containment for better performance
|
||||
contain: 'layout style paint',
|
||||
contentVisibility: 'auto',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
<Link
|
||||
key={cardId}
|
||||
href={videoUrl}
|
||||
onClick={(e) => onCardClick(e, cardId, videoUrl)}
|
||||
@@ -59,7 +59,8 @@ const VideoCard = memo(({
|
||||
<Card
|
||||
className="p-0 overflow-hidden group cursor-pointer flex flex-col h-full bg-[var(--bg-color)]/50 backdrop-blur-none saturate-100 shadow-sm border-[var(--glass-border)] hover:shadow-lg transition-shadow"
|
||||
hover={false} // Disable default hover scale to improve performance
|
||||
style={{
|
||||
blur={false}
|
||||
style={{
|
||||
willChange: 'transform',
|
||||
transform: 'translate3d(0,0,0)',
|
||||
backfaceVisibility: 'hidden',
|
||||
@@ -75,7 +76,7 @@ const VideoCard = memo(({
|
||||
className="object-cover rounded-[var(--radius-2xl)]"
|
||||
sizes="(max-width: 640px) 33vw, (max-width: 1024px) 20vw, 16vw"
|
||||
loading="lazy"
|
||||
unoptimized={true} // Skip server-side optimization for better scroll performance on localhost
|
||||
// unoptimized={true} // Removed for performance - let Next.js optimize images
|
||||
onError={(e) => {
|
||||
// Fallback for next/image error is tricky because it doesn't expose the img element directly in the same way
|
||||
// But we can try to hide it or show a placeholder
|
||||
@@ -95,12 +96,12 @@ const VideoCard = memo(({
|
||||
<Icons.Film size={64} className="text-[var(--text-color-secondary)]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Fallback Icon (always rendered behind image, visible if image fails/loads) */}
|
||||
<div className="absolute inset-0 flex items-center justify-center -z-10">
|
||||
<Icons.Film size={64} className="text-[var(--text-color-secondary)] opacity-20" />
|
||||
<Icons.Film size={64} className="text-[var(--text-color-secondary)] opacity-20" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Badge Container - Top, spans full width with proper spacing */}
|
||||
<div className="absolute top-2 left-2 right-2 z-10 flex items-center justify-between gap-1">
|
||||
{/* Source Badge - Left */}
|
||||
@@ -109,19 +110,18 @@ const VideoCard = memo(({
|
||||
{video.sourceName}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
|
||||
{/* Latency Badge - Right */}
|
||||
{video.latency !== undefined && (
|
||||
<LatencyBadge latency={video.latency} className="flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Overlay - Show on hover (desktop) or when active (mobile) */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/60 transition-opacity duration-300 ${
|
||||
isActive ? 'opacity-100 lg:opacity-0 lg:group-hover:opacity-100' : 'opacity-0 lg:group-hover:opacity-100'
|
||||
}`}
|
||||
style={{
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/60 transition-opacity duration-300 ${isActive ? 'opacity-100 lg:opacity-0 lg:group-hover:opacity-100' : 'opacity-0 lg:group-hover:opacity-100'
|
||||
}`}
|
||||
style={{
|
||||
willChange: 'opacity',
|
||||
}}
|
||||
>
|
||||
@@ -147,19 +147,19 @@ const VideoCard = memo(({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info - Fixed height section */}
|
||||
<div className="p-3 flex-1 flex flex-col">
|
||||
<h4 className="font-semibold text-sm text-[var(--text-color)] line-clamp-2 min-h-[2.5rem]">
|
||||
{video.vod_name}
|
||||
</h4>
|
||||
{video.vod_remarks && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1 line-clamp-1">
|
||||
{video.vod_remarks}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
{/* Info - Fixed height section */}
|
||||
<div className="p-3 flex-1 flex flex-col">
|
||||
<h4 className="font-semibold text-sm text-[var(--text-color)] line-clamp-2 min-h-[2.5rem]">
|
||||
{video.vod_name}
|
||||
</h4>
|
||||
{video.vod_remarks && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1 line-clamp-1">
|
||||
{video.vod_remarks}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -171,7 +171,7 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid
|
||||
const [visibleCount, setVisibleCount] = useState(24);
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
|
||||
|
||||
if (videos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -179,14 +179,14 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid
|
||||
// Callback ref for the load more trigger to handle dynamic mounting/unmounting
|
||||
const loadMoreRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (observerRef.current) observerRef.current.disconnect();
|
||||
|
||||
|
||||
if (node) {
|
||||
observerRef.current = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
setVisibleCount(prev => prev + 24);
|
||||
}
|
||||
}, { rootMargin: '400px' });
|
||||
|
||||
|
||||
observerRef.current.observe(node);
|
||||
}
|
||||
}, []);
|
||||
@@ -195,7 +195,7 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid
|
||||
const handleCardClick = useCallback((e: React.MouseEvent, videoId: string, videoUrl: string) => {
|
||||
// Check if it's a mobile device
|
||||
const isMobile = window.innerWidth < 1024; // lg breakpoint
|
||||
|
||||
|
||||
if (isMobile) {
|
||||
// On mobile, first click shows details, second click navigates
|
||||
if (activeCardId === videoId) {
|
||||
@@ -218,9 +218,9 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid
|
||||
source: video.source,
|
||||
title: video.vod_name,
|
||||
}).toString()}`;
|
||||
|
||||
|
||||
const cardId = `${video.vod_id}-${index}`;
|
||||
|
||||
|
||||
return {
|
||||
video,
|
||||
videoUrl,
|
||||
@@ -233,21 +233,21 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
<div
|
||||
ref={gridRef}
|
||||
className={`grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-6 gap-3 md:gap-4 lg:gap-6 max-w-[1920px] mx-auto ${className}`}
|
||||
role="list"
|
||||
aria-label="视频搜索结果"
|
||||
style={{
|
||||
// Optimize rendering performance
|
||||
willChange: 'auto',
|
||||
// willChange: 'auto', // Removed to let browser decide
|
||||
contain: 'layout style paint',
|
||||
contentVisibility: 'auto',
|
||||
}}
|
||||
>
|
||||
{visibleItems.map(({ video, videoUrl, cardId }) => {
|
||||
const isActive = activeCardId === cardId;
|
||||
|
||||
|
||||
return (
|
||||
<VideoCard
|
||||
key={cardId}
|
||||
@@ -260,11 +260,11 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Load more trigger */}
|
||||
{visibleCount < videoItems.length && (
|
||||
<div
|
||||
ref={loadMoreRef}
|
||||
<div
|
||||
ref={loadMoreRef}
|
||||
className="h-20 w-full flex items-center justify-center opacity-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
+11
-8
@@ -4,20 +4,23 @@ interface CardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
hover?: boolean;
|
||||
blur?: boolean;
|
||||
onClick?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Card({ children, className = '', hover = true, onClick, style }: CardProps) {
|
||||
const hoverStyles = hover
|
||||
? "hover:translate-y-[-2px] hover:shadow-[0_8px_24px_var(--shadow-color)] cursor-pointer transition-transform duration-200 ease-out"
|
||||
export function Card({ children, className = '', hover = true, blur = true, onClick, style }: CardProps) {
|
||||
const hoverStyles = hover
|
||||
? "hover:translate-y-[-2px] hover:shadow-[0_8px_24px_var(--shadow-color)] cursor-pointer transition-transform duration-200 ease-out"
|
||||
: "";
|
||||
|
||||
// Conditionally apply blur classes - NOW DISABLED GLOBALLY via CSS override, but logic kept for structure
|
||||
const blurClasses = blur
|
||||
? "bg-[var(--glass-bg)]" // Removed backdrop-blur classes here as they are expensive
|
||||
: "bg-[var(--bg-color)]/90"; // More opaque fallback
|
||||
|
||||
const baseClasses = `
|
||||
bg-[var(--glass-bg)]
|
||||
backdrop-blur-[12px]
|
||||
saturate-[120%]
|
||||
[-webkit-backdrop-filter:blur(12px)_saturate(120%)]
|
||||
${blurClasses}
|
||||
rounded-[var(--radius-2xl)]
|
||||
shadow-[0_2px_8px_var(--shadow-color)] md:shadow-[var(--shadow-md)]
|
||||
border
|
||||
@@ -31,7 +34,7 @@ export function Card({ children, className = '', hover = true, onClick, style }:
|
||||
// Use semantic button when interactive
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`${baseClasses} text-left w-full`}
|
||||
|
||||
Reference in New Issue
Block a user