refactor: Remove player-specific dependencies, setup script, keyboard shortcuts, and project documentation.

This commit is contained in:
kuekhaoyang
2025-11-22 10:56:11 +08:00
parent 8465b306f2
commit 5c67f69b5e
17 changed files with 27 additions and 1452 deletions
-774
View File
@@ -1,774 +0,0 @@
# 贡献指南
感谢您对 KVideo 项目的关注!我们热烈欢迎任何形式的贡献,包括但不限于:
- 🐛 报告 Bug
- 💡 提出新功能建议
- 📝 改进文档
- 🎨 优化 UI/UX
- ✨ 提交代码修复或新功能
## 目录
- [行为准则](#行为准则)
- [如何贡献](#如何贡献)
- [报告 Bug](#报告-bug)
- [提出功能建议](#提出功能建议)
- [提交代码](#提交代码)
- [开发指南](#开发指南)
- [环境搭建](#环境搭建)
- [代码规范](#代码规范)
- [提交规范](#提交规范)
- [Liquid Glass UI 设计规范](#liquid-glass-ui-设计规范)
- [核心原则](#核心原则)
- [组件设计准则](#组件设计准则)
- [CSS 变量系统](#css-变量系统)
- [动画规范](#动画规范)
- [测试指南](#测试指南)
- [文档规范](#文档规范)
## 行为准则
本项目遵循 [Contributor Covenant](https://www.contributor-covenant.org/) 行为准则。参与本项目即表示您同意遵守其条款。
我们承诺提供一个开放、友好、包容的社区环境:
- 尊重不同的观点和经验
- 优雅地接受建设性批评
- 关注对社区最有利的事情
- 对其他社区成员保持同理心
## 如何贡献
### 报告 Bug
如果您发现了 Bug,请通过 [GitHub Issues](https://github.com/KuekHaoYang/kvideo/issues) 报告。报告时请包含:
1. **清晰的标题** - 简明扼要地描述问题
2. **重现步骤** - 详细说明如何触发 Bug
3. **预期行为** - 描述您期望的正常行为
4. **实际行为** - 描述实际发生了什么
5. **环境信息**
- 浏览器版本(如 Chrome 120
- 操作系统(如 macOS 14.0
- Node.js 版本(如 20.10.0
6. **截图/视频**(如适用)
7. **控制台错误**(如有)
**Bug 报告模板:**
```markdown
### 问题描述
[清晰描述 Bug]
### 重现步骤
1. 进入 '...'
2. 点击 '...'
3. 滚动到 '...'
4. 看到错误
### 预期行为
[描述预期的正常行为]
### 实际行为
[描述实际发生的情况]
### 截图
[如果适用,添加截图]
### 环境
- 浏览器: [如 Chrome 120]
- 操作系统: [如 macOS 14.0]
- Node.js 版本: [如 20.10.0]
### 额外信息
[任何其他有助于解决问题的信息]
```
### 提出功能建议
我们欢迎新功能建议!请通过 [GitHub Issues](https://github.com/KuekHaoYang/kvideo/issues) 提交,并包含:
1. **功能概述** - 简要描述功能
2. **使用场景** - 说明为什么需要这个功能
3. **详细设计** - 描述功能如何工作
4. **UI 设计**(如适用)- 提供设计稿或草图
5. **技术实现思路**(可选)
### 提交代码
1. **Fork 仓库**
```bash
# 点击 GitHub 页面右上角的 "Fork" 按钮
```
2. **克隆您的 Fork**
```bash
git clone https://github.com/YOUR_USERNAME/kvideo.git
cd kvideo
```
3. **创建特性分支**
```bash
git checkout -b feature/your-feature-name
# 或
git checkout -b fix/your-bug-fix
```
4. **安装依赖**
```bash
npm install
```
5. **进行开发**
- 遵循 [代码规范](#代码规范)
- 遵循 [Liquid Glass UI 设计规范](#liquid-glass-ui-设计规范)
- 编写清晰的代码注释
6. **测试您的更改**
```bash
npm run dev # 启动开发服务器
npm run lint # 检查代码规范
npm run build # 确保构建成功
```
7. **提交更改**
```bash
git add .
git commit -m "feat: 添加某个功能"
# 遵循提交规范(见下文)
```
8. **推送到您的 Fork**
```bash
git push origin feature/your-feature-name
```
9. **创建 Pull Request**
- 前往原仓库页面
- 点击 "New Pull Request"
- 填写 PR 模板
- 等待代码审查
## 开发指南
### 环境搭建
**系统要求:**
- Node.js 20.x 或更高
- npm 9.x 或 pnpm 8.x
- Git 2.x
**快速开始:**
```bash
# 克隆仓库
git clone https://github.com/YOUR_USERNAME/kvideo.git
cd kvideo
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 打开浏览器访问 http://localhost:3000
```
### 代码规范
#### TypeScript 规范
- **严格模式** - 启用 `strict: true`
- **类型注解** - 所有函数参数和返回值必须有类型
- **避免 `any`** - 使用具体类型或 `unknown`
- **接口优先** - 优先使用 `interface` 而非 `type`
```typescript
// ✅ 好的示例
interface VideoProps {
id: string;
title: string;
onPlay: (url: string) => void;
}
function VideoCard({ id, title, onPlay }: VideoProps): JSX.Element {
return <div>{title}</div>;
}
// ❌ 不好的示例
function VideoCard(props: any) {
return <div>{props.title}</div>;
}
```
#### React 组件规范
- **函数组件** - 使用函数组件和 Hooks
- **命名规范** - PascalCase 命名组件文件
- **单一职责** - 每个组件只做一件事
- **文件大小** - 单个文件不超过 150 行(严格遵守)
- **Props 解构** - 在函数参数中解构 props
```typescript
// ✅ 好的示例 - SearchForm.tsx
'use client';
interface SearchFormProps {
onSubmit: (query: string) => void;
placeholder?: string;
}
export function SearchForm({ onSubmit, placeholder = '搜索视频...' }: SearchFormProps) {
// 组件逻辑(不超过 150 行)
}
```
#### 文件组织规范
```
components/
├── search/ # 功能分组
│ ├── SearchForm.tsx # 主组件
│ ├── VideoGrid.tsx
│ └── index.ts # 导出文件
├── player/
└── ui/ # 通用 UI 组件
├── Button.tsx
├── Card.tsx
└── Input.tsx
```
#### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件 | PascalCase | `VideoPlayer`, `SearchForm` |
| 函数 | camelCase | `handleSearch`, `fetchVideoData` |
| 常量 | UPPER_SNAKE_CASE | `API_BASE_URL`, `MAX_RESULTS` |
| 接口 | PascalCase + 描述性 | `VideoPlayerProps`, `SearchResult` |
| 类型 | PascalCase | `VideoData`, `PlayerState` |
| Hook | use + PascalCase | `useVideoPlayer`, `useSearchCache` |
### 提交规范
我们遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
**格式:**
```
<type>(<scope>): <subject>
<body>
<footer>
```
**类型(type):**
| 类型 | 说明 |
|------|------|
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `docs` | 文档更新 |
| `style` | 代码格式(不影响功能) |
| `refactor` | 重构(既不是新功能也不是 Bug 修复) |
| `perf` | 性能优化 |
| `test` | 添加测试 |
| `chore` | 构建过程或辅助工具的变动 |
| `ui` | UI/UX 改进 |
**示例:**
```bash
# 新功能
git commit -m "feat(search): 添加实时流式搜索功能"
# Bug 修复
git commit -m "fix(player): 修复 HLS 流加载失败问题"
# UI 改进
git commit -m "ui(card): 优化视频卡片悬停动画效果"
# 文档
git commit -m "docs(readme): 更新安装步骤说明"
# 重构
git commit -m "refactor(api): 提取搜索逻辑到独立模块"
```
## Liquid Glass UI 设计规范
KVideo 严格遵循 **Liquid Glass** 设计系统。所有 UI 贡献必须符合以下规范。
### 核心原则
#### 1. 玻璃效果(Glass Effect
所有容器类组件必须使用毛玻璃效果:
```css
.glass-container {
background: var(--glass-bg);
backdrop-filter: blur(25px) saturate(180%);
-webkit-backdrop-filter: blur(25px) saturate(180%);
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-md);
}
```
#### 2. 通用柔软度(Universal Softness
**只使用两种圆角:**
- **`rounded-2xl` (1.5rem)** - 用于容器类组件
- **`rounded-full` (9999px)** - 用于圆形/胶囊组件
```typescript
// ✅ 正确示例
<div className="... rounded-2xl"> {/* 卡片 */}
<button className="... rounded-2xl"> {/* 按钮 */}
<input className="... rounded-2xl"> {/* 输入框 */}
<div className="... rounded-full"> {/* 头像 */}
<span className="... rounded-full"> {/* 徽章 */}
// ❌ 错误示例
<div className="... rounded-lg"> {/* 不使用其他圆角值 */}
<div className="... rounded-md">
<div className="... rounded">
```
#### 3. 流体动画(Fluid Animation
使用物理感知的缓动曲线:
```css
.animated-element {
transition: all var(--transition-fluid);
/* 等价于: transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); */
}
```
#### 4. 光学交互(Lensing & Light Interaction
悬停时添加内发光效果:
```css
.interactive-element:hover {
transform: translateY(-2px);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 30%, transparent);
}
```
#### 5. 层次分明(Depth & Hierarchy
使用两级阴影:
```css
--shadow-sm: 0 2px 4px var(--shadow-color);
--shadow-md: 0 4px 12px var(--shadow-color);
```
### 组件设计准则
#### 按钮组件
```typescript
// Button.tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant = 'primary', size = 'md', children, onClick }: ButtonProps) {
return (
<button
className={`
inline-flex items-center justify-center
font-semibold transition-all duration-[400ms]
rounded-2xl shadow-[var(--shadow-sm)]
hover:transform hover:translate-y-[-2px]
hover:shadow-[var(--shadow-md)]
active:transform active:translate-y-0 active:scale-[0.98]
${variant === 'primary' && 'bg-[var(--accent-color)] text-white'}
${variant === 'secondary' && 'bg-[var(--glass-bg)] border border-[var(--glass-border)]'}
${size === 'md' && 'px-5 py-3 text-base'}
`}
onClick={onClick}
>
{children}
</button>
);
}
```
#### 卡片组件
```typescript
// Card.tsx
interface CardProps {
children: React.ReactNode;
hover?: boolean;
}
export function Card({ children, hover = true }: CardProps) {
return (
<div
className={`
bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%]
border border-[var(--glass-border)] rounded-2xl
shadow-[var(--shadow-md)] p-6
transition-all duration-[400ms]
${hover && 'hover:transform hover:translate-y-[-5px] hover:scale-[1.02]'}
`}
>
{children}
</div>
);
}
```
#### 输入框组件
```typescript
// Input.tsx
interface InputProps {
placeholder?: string;
value: string;
onChange: (value: string) => void;
}
export function Input({ placeholder, value, onChange }: InputProps) {
return (
<input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
className="
w-full bg-[var(--glass-bg)] backdrop-blur-[10px]
border border-[var(--glass-border)] rounded-2xl
px-4 py-3 text-base text-[var(--text-color)]
transition-all duration-[400ms]
focus:outline-none focus:border-[var(--accent-color)]
focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)]
"
/>
);
}
```
### CSS 变量系统
所有样式必须使用 CSS 变量,确保主题切换正常工作:
```css
/* globals.css */
:root {
/* 字体 */
--font-family-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* 浅色主题 */
--bg-color-light: #f0f2f5;
--text-color-light: #1d1d1f;
--accent-color-light: #007aff;
--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);
/* 深色主题 */
--bg-color-dark: #121212;
--text-color-dark: #f5f5f7;
--accent-color-dark: #0a84ff;
--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);
/* 圆角 */
--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);
}
body {
--bg-color: var(--bg-color-light);
--text-color: var(--text-color-light);
--accent-color: var(--accent-color-light);
--glass-bg: var(--glass-bg-light);
--glass-border: var(--glass-border-light);
--shadow-color: var(--shadow-color-light);
}
body.dark-mode {
--bg-color: var(--bg-color-dark);
--text-color: var(--text-color-dark);
--accent-color: var(--accent-color-dark);
--glass-bg: var(--glass-bg-dark);
--glass-border: var(--glass-border-dark);
--shadow-color: var(--shadow-color-dark);
}
```
**使用示例:**
```typescript
// ✅ 正确 - 使用 CSS 变量
<div style={{
background: 'var(--glass-bg)',
borderRadius: 'var(--radius-2xl)',
color: 'var(--text-color)'
}} />
// ❌ 错误 - 硬编码颜色
<div style={{
background: '#f0f2f5',
borderRadius: '1.5rem',
color: '#1d1d1f'
}} />
```
### 动画规范
#### 悬停动画
```typescript
<div className="
transition-all duration-[400ms]
hover:transform hover:translate-y-[-5px] hover:scale-[1.02]
hover:shadow-[var(--shadow-md)]
">
```
#### 点击动画
```typescript
<button className="
active:transform active:translate-y-0 active:scale-[0.98]
">
```
#### 淡入动画
```typescript
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
```
### 响应式设计
使用 Tailwind 的响应式前缀:
```typescript
<div className="
grid grid-cols-1 gap-4
sm:grid-cols-2
md:grid-cols-3
lg:grid-cols-4
xl:grid-cols-5
">
```
**断点定义:**
| 前缀 | 最小宽度 | 适用设备 |
|------|---------|---------|
| `sm` | 640px | 平板竖屏 |
| `md` | 768px | 平板横屏 |
| `lg` | 1024px | 笔记本 |
| `xl` | 1280px | 桌面 |
| `2xl` | 1536px | 大屏 |
### UI 检查清单
提交 UI 相关的 PR 前,请确保:
- [ ] 所有容器使用 `rounded-2xl`
- [ ] 所有圆形元素使用 `rounded-full`
- [ ] 使用毛玻璃效果 `backdrop-filter: blur(25px) saturate(180%)`
- [ ] 使用 CSS 变量而非硬编码颜色
- [ ] 悬停时有流体动画效果
- [ ] 支持深浅色主题
- [ ] 在移动端和桌面端测试过
- [ ] 无控制台警告或错误
- [ ] 代码通过 ESLint 检查
## 测试指南
### 手动测试
在提交 PR 前,请确保:
1. **功能测试**
- [ ] 新功能按预期工作
- [ ] 没有破坏现有功能
- [ ] 边界情况处理正确
2. **浏览器测试**
- [ ] Chrome(最新版本)
- [ ] Safari(最新版本)
- [ ] Firefox(最新版本)
- [ ] Edge(最新版本)
3. **响应式测试**
- [ ] 移动端(375px - 428px
- [ ] 平板(768px - 1024px
- [ ] 桌面(1280px+
4. **主题测试**
- [ ] 浅色主题显示正常
- [ ] 深色主题显示正常
- [ ] 主题切换平滑
5. **性能测试**
- [ ] 页面加载时间 < 3 秒
- [ ] 动画流畅(60fps
- [ ] 无内存泄漏
### 构建测试
```bash
# 开发环境
npm run dev
# 生产构建
npm run build
# 检查构建产物
npm start
# 代码检查
npm run lint
```
## 文档规范
### 代码注释
- **函数注释** - 使用 JSDoc 格式
- **复杂逻辑** - 添加行内注释说明
- **TODO** - 使用 `// TODO:` 标记待办事项
```typescript
/**
* 并行搜索视频
* @param query - 搜索关键词
* @param sources - 视频源列表
* @returns 搜索结果数组
*/
async function searchVideos(
query: string,
sources: VideoSource[]
): Promise<SearchResult[]> {
// TODO: 添加缓存机制
const results = await Promise.all(
sources.map(source => fetchFromSource(source, query))
);
return results.filter(Boolean);
}
```
### README 更新
如果您的更改影响到以下内容,请更新 README:
- 添加新功能
- 修改安装步骤
- 更新依赖项
- 添加新的 API
## Pull Request 指南
### PR 标题
遵循 Conventional Commits 格式:
```
feat(search): 添加实时流式搜索功能
fix(player): 修复视频加载失败问题
docs(readme): 更新安装步骤
ui(card): 优化卡片悬停效果
```
### PR 描述模板
```markdown
## 变更类型
- [ ] 新功能
- [ ] Bug 修复
- [ ] 性能优化
- [ ] 重构
- [ ] 文档更新
- [ ] UI/UX 改进
## 变更描述
[清晰描述您做了什么改动]
## 相关 Issue
Closes #[issue 编号]
## 测试
- [ ] 本地测试通过
- [ ] 多浏览器测试
- [ ] 响应式测试
- [ ] 主题切换测试
## 截图
[如果是 UI 改动,添加前后对比截图]
## 检查清单
- [ ] 代码遵循项目规范
- [ ] 已添加必要的注释
- [ ] 已更新相关文档
- [ ] 无 ESLint 警告
- [ ] 通过所有测试
- [ ] UI 符合 Liquid Glass 设计规范
```
### 代码审查
PR 提交后,维护者会进行代码审查。请:
- 及时回复审查意见
- 根据反馈修改代码
- 保持讨论友好和专业
- 学习和理解审查意见
## 获得帮助
如果您在贡献过程中遇到问题:
1. **查看文档** - 阅读 README 和本贡献指南
2. **搜索 Issues** - 查看是否有类似问题
3. **提问** - 在 [GitHub Discussions](https://github.com/KuekHaoYang/kvideo/discussions) 提问
4. **联系维护者** - 通过 Issues 联系项目维护者
## 致谢
感谢所有为 KVideo 做出贡献的开发者!您的努力让这个项目变得更好。
---
<p align="center">
再次感谢您的贡献!✨<br>
让我们一起打造最优雅的视频聚合平台
</p>
-410
View File
@@ -1,410 +0,0 @@
# KVideo
> 基于 Liquid Glass 设计理念的现代化流媒体视频聚合平台
[![Next.js](https://img.shields.io/badge/Next.js-16.0-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19.2-blue?style=flat-square&logo=react)](https://reactjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.0-38B2AC?style=flat-square&logo=tailwind-css)](https://tailwindcss.com/)
[![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)](LICENSE)
## 目录
- [关于项目](#关于项目)
- [核心特性](#核心特性)
- [技术栈](#技术栈)
- [设计理念](#设计理念)
- [快速开始](#快速开始)
- [系统要求](#系统要求)
- [安装步骤](#安装步骤)
- [使用指南](#使用指南)
- [开发模式](#开发模式)
- [生产构建](#生产构建)
- [核心功能](#核心功能)
- [智能并行搜索](#智能并行搜索)
- [实时流式传输](#实时流式传输)
- [源可用性检测](#源可用性检测)
- [观看历史管理](#观看历史管理)
- [自适应视频播放器](#自适应视频播放器)
- [项目架构](#项目架构)
- [贡献指南](#贡献指南)
- [许可证](#许可证)
- [联系方式](#联系方式)
## 关于项目
KVideo 是一个现代化的流媒体视频聚合平台,采用 **Liquid Glass** 设计系统打造极致的用户体验。平台通过智能并行搜索技术,实时聚合多个视频源的内容,为用户提供流畅、直观且视觉震撼的观影体验。
### 核心特性
- 🔍 **智能并行搜索** - 同时查询 15+ 视频源,实时流式返回结果
- 🎬 **自适应播放器** - 支持 HLS/M3U8 流,自动源切换,断点续播
- 🎨 **Liquid Glass UI** - 毛玻璃效果,流体动画,深浅色主题无缝切换
-**实时可用性检测** - 搜索时自动过滤失效源,确保播放成功率
- 📊 **类型智能筛选** - 自动识别分类(电影/剧集/综艺),支持多选过滤
- 📚 **观看历史追踪** - 自动记录播放进度,快速恢复观看
- 🌐 **无服务器架构** - 基于 Next.js App RouterAPI Routes 处理所有后端逻辑
- 📱 **响应式设计** - 完美适配桌面、平板、移动设备
### 技术栈
**前端框架**
- [Next.js 16.0](https://nextjs.org/) - React 元框架,提供服务端渲染和路由
- [React 19.2](https://reactjs.org/) - 用户界面构建库
- [TypeScript 5.x](https://www.typescriptlang.org/) - 类型安全的 JavaScript 超集
**样式系统**
- [Tailwind CSS 4.0](https://tailwindcss.com/) - 实用优先的 CSS 框架
- 自定义 Liquid Glass CSS 变量系统
**状态管理 & 工具**
- [Zustand 5.0](https://github.com/pmndrs/zustand) - 轻量级状态管理
- [Artplayer 5.1](https://artplayer.org/) - 现代化 HTML5 视频播放器
- [HLS.js 1.5](https://github.com/video-dev/hls.js/) - HLS 流协议支持
**视频源整合**
- 电影天堂、如意、暴风、天涯等 15+ 第三方视频 API
- 自定义源配置系统,支持动态添加
### 设计理念
KVideo 严格遵循 **Liquid Glass** 设计系统,灵感源自 Apple 的 visionOS 和 Jony Ive 的极简主义哲学:
1. **玻璃效果** - 毛玻璃材质(`backdrop-filter: blur(25px) saturate(180%)`)营造深度感
2. **通用柔软度** - 所有元素采用 `rounded-2xl``rounded-full` 圆角,无硬边
3. **流体动画** - 物理感知的缓动曲线(`cubic-bezier(0.2, 0.8, 0.2, 1)`
4. **光学交互** - 悬停时内发光效果,仿佛组件在捕捉光线
5. **层次分明** - 清晰的 Z 轴深度,交互层始终位于视觉顶部
## 快速开始
### 系统要求
- **Node.js** 20.x 或更高版本
- **npm** 或 **pnpm** 包管理器
- 现代浏览器(Chrome 90+、Safari 14+、Firefox 88+
### 安装步骤
1. **克隆仓库**
```bash
git clone https://github.com/KuekHaoYang/kvideo.git
cd kvideo
```
2. **安装依赖**
```bash
npm install
# 或使用 pnpm
pnpm install
```
3. **启动开发服务器**
```bash
npm run dev
```
4. **访问应用**
打开浏览器访问 [http://localhost:3000](http://localhost:3000)
## 使用指南
### 开发模式
开发模式支持热重载和快速调试:
```bash
npm run dev
```
应用将在 `http://localhost:3000` 启动,代码更改会自动刷新页面。
### 生产构建
构建优化后的生产版本:
```bash
# 构建应用
npm run build
# 启动生产服务器
npm start
```
### 代码检查
运行 ESLint 检查代码质量:
```bash
npm run lint
```
## 核心功能
### 智能并行搜索
KVideo 采用先进的并行搜索架构,同时查询多个视频源:
- **并发请求** - 15 个视频源同时搜索,无需等待
- **流式传输** - 结果实时流式返回,即查即得
- **缓存机制** - 搜索结果本地缓存,秒开历史查询
- **相关性排序** - 智能匹配算法,最相关内容优先展示
```typescript
// 核心搜索 Hook - useParallelSearch
const { results, loading, performSearch } = useParallelSearch(
saveToCache,
onUrlUpdate
);
performSearch('电影名称'); // 触发并行搜索
```
### 实时流式传输
搜索过程采用服务端推送(SSE)技术:
1. **搜索阶段** - 显示已完成源数量 / 总源数量
2. **检测阶段** - 显示已验证视频数 / 总视频数
3. **结果推送** - 每验证通过一批视频立即推送
```typescript
// API Route - /api/search-stream
// 返回格式:
// data: {"type": "progress", "stage": "searching", "checkedSources": 5}
// data: {"type": "videos", "videos": [...], "checkedVideos": 10}
// data: {"type": "complete", "totalResults": 120}
```
### 源可用性检测
搜索时自动验证视频源可用性,过滤无效链接:
- **URL 格式验证** - 检查链接是否符合 M3U8/MP4 规范
- **HEAD 请求预检** - 验证资源是否存在(状态码 200/206)
- **内容大小检测** - 确保内容大小 > 1KB,排除空文件
- **并发控制** - 同时检测 8 个链接,平衡速度与服务器压力
```typescript
// 源检测核心函数
const availableVideos = await checkMultipleVideos(allVideos, 8);
// 仅返回可播放的视频
```
### 观看历史管理
基于 Zustand 的持久化历史记录:
- **自动记录** - 播放时自动保存到 localStorage
- **进度追踪** - 记录每集观看进度,支持断点续播
- **侧边栏展示** - 快速访问最近观看的视频
- **一键清除** - 支持批量或单个删除历史
```typescript
// 历史存储 Store
const { addToHistory, clearHistory } = useHistoryStore();
addToHistory(videoId, title, playUrl, source, episodeName, currentTime);
```
### 自适应视频播放器
基于 Artplayer 构建的高级播放器:
- **HLS 流支持** - 集成 hls.js,无缝播放 M3U8 格式
- **自动源切换** - 播放失败时自动尝试备用源
- **倍速播放** - 0.5x - 2x 速度调节
- **画质选择** - 自动识别多码率流
- **全屏控制** - 支持网页全屏和系统全屏
- **快捷键支持** - 空格暂停、方向键快进/快退
```typescript
// 播放器核心 Hook - useVideoPlayer
const { videoData, playUrl, currentEpisode } = useVideoPlayer(
videoId,
source,
episodeParam
);
```
## 项目架构
```
kvideo/
├── app/ # Next.js App Router
│ ├── api/ # API Routes
│ │ ├── search/ # 标准搜索 API
│ │ ├── search-stream/ # 流式搜索 API
│ │ ├── search-parallel/ # 并行搜索 API
│ │ ├── detail/ # 视频详情 API
│ │ └── hot/ # 热门推荐 API
│ ├── player/ # 播放器页面
│ ├── history/ # 历史记录页面
│ ├── layout.tsx # 根布局组件
│ ├── page.tsx # 首页/搜索页
│ └── globals.css # Liquid Glass 全局样式
├── components/ # React 组件
│ ├── search/ # 搜索相关组件
│ │ ├── SearchForm.tsx # 搜索表单
│ │ ├── VideoGrid.tsx # 视频网格布局
│ │ ├── TypeBadges.tsx # 类型筛选徽章
│ │ └── ResultsHeader.tsx # 搜索结果头部
│ ├── player/ # 播放器组件
│ │ ├── VideoPlayer.tsx # 主播放器
│ │ ├── EpisodeList.tsx # 剧集列表
│ │ └── VideoMetadata.tsx # 视频元数据
│ ├── history/ # 历史记录组件
│ │ └── WatchHistorySidebar.tsx
│ ├── home/ # 首页组件
│ │ └── PopularFeatures.tsx
│ ├── ThemeProvider.tsx # 主题提供器
│ └── ThemeSwitcher.tsx # 主题切换器
├── lib/ # 核心逻辑库
│ ├── api/ # API 客户端
│ │ ├── client.ts # HTTP 请求封装
│ │ └── video-sources.ts # 视频源配置
│ ├── hooks/ # 自定义 React Hooks
│ │ ├── useParallelSearch.ts # 并行搜索 Hook
│ │ ├── useSearchStream.ts # 流式搜索 Hook
│ │ ├── useVideoPlayer.ts # 播放器 Hook
│ │ ├── useTypeBadges.ts # 类型筛选 Hook
│ │ └── useSearchCache.ts # 搜索缓存 Hook
│ ├── store/ # Zustand 状态管理
│ │ ├── history-store.ts # 历史记录 Store
│ │ ├── player-store.ts # 播放器 Store
│ │ └── search-history-store.ts # 搜索历史 Store
│ ├── types/ # TypeScript 类型定义
│ │ └── index.ts
│ └── utils/ # 工具函数
│ ├── source-checker.ts # 源可用性检测
│ ├── url-validator.ts # URL 验证
│ ├── m3u8-filter.ts # M3U8 过滤
│ ├── episode-manager.ts # 剧集管理
│ └── progress-tracker.ts # 进度追踪
├── public/ # 静态资源
├── next.config.ts # Next.js 配置
├── tailwind.config.ts # Tailwind 配置
├── tsconfig.json # TypeScript 配置
└── package.json # 项目依赖
```
### 核心模块说明
#### API Routes/app/api
- **search/** - 标准搜索,返回完整结果
- **search-stream/** - SSE 流式搜索,实时推送
- **search-parallel/** - 并行搜索,最快响应
- **detail/** - 获取视频详细信息和播放链接
#### Hooks/lib/hooks
- **useParallelSearch** - 并行搜索管理,状态同步
- **useVideoPlayer** - 播放器状态、剧集切换
- **useTypeBadges** - 类型筛选逻辑
- **useSearchCache** - localStorage 缓存管理
#### Utils/lib/utils
- **source-checker** - 视频源健康检查
- **url-validator** - URL 格式验证
- **m3u8-filter** - M3U8 播放列表过滤
- **episode-manager** - 剧集解析与排序
## 贡献指南
我们热烈欢迎社区贡献!无论是修复 Bug、新增功能还是改进文档,您的参与都将使 KVideo 变得更好。
在开始贡献之前,请仔细阅读我们的 **[贡献指南(CONTRIBUTING.md](CONTRIBUTING.md)**,其中包含:
- 📋 完整的贡献流程
- 💻 代码规范和最佳实践
- 🎨 Liquid Glass UI 设计规范详解
- ✅ PR 提交检查清单
- 🧪 测试指南
### 快速开始
1. **Fork 本仓库并克隆**
```bash
git clone https://github.com/YOUR_USERNAME/kvideo.git
cd kvideo
```
2. **创建特性分支**
```bash
git checkout -b feature/your-feature-name
```
3. **安装依赖并开发**
```bash
npm install
npm run dev
```
4. **提交更改(遵循 Conventional Commits**
```bash
git commit -m "feat: 添加某个功能"
```
5. **推送并创建 Pull Request**
```bash
git push origin feature/your-feature-name
```
### 核心规范速览
**代码规范:**
- ✅ TypeScript 严格模式
- ✅ 单个文件不超过 150 行
- ✅ 遵循 [Conventional Commits](https://www.conventionalcommits.org/)
**Liquid Glass UI 设计规范:**
- ✅ 容器类组件使用 `rounded-2xl`1.5rem
- ✅ 圆形/胶囊组件使用 `rounded-full`
- ✅ 毛玻璃效果:`backdrop-filter: blur(25px) saturate(180%)`
- ✅ 流体动画:`cubic-bezier(0.2, 0.8, 0.2, 1)`
- ✅ 使用 CSS 变量而非硬编码颜色
详细规范请查看 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 许可证
本项目采用 **MIT 许可证**。详见 [LICENSE](LICENSE) 文件。
这意味着您可以自由地:
- ✅ 商业使用
- ✅ 修改源代码
- ✅ 分发副本
- ✅ 私人使用
唯一的要求是在所有副本或重要部分中包含版权声明和许可证声明。
```
MIT License - Copyright (c) 2025 Kuek Hao Yang
```
## 联系方式
- **作者:** Kuek Hao Yang
- **GitHub** [@KuekHaoYang](https://github.com/KuekHaoYang)
- **项目地址:** [https://github.com/KuekHaoYang/kvideo](https://github.com/KuekHaoYang/kvideo)
### 获取帮助
- 🐛 **报告 Bug** [提交 Issue](https://github.com/KuekHaoYang/kvideo/issues/new)
- 💡 **功能建议:** [发起讨论](https://github.com/KuekHaoYang/kvideo/discussions)
- 🤝 **贡献代码:** 查看 [贡献指南](CONTRIBUTING.md)
- 📖 **文档问题:** 通过 Issues 反馈
---
<p align="center">
使用 ❤️ 和 <strong>Liquid Glass</strong> 设计系统打造<br>
<em>让每一帧画面都如同触摸玻璃般流畅</em>
</p>
-50
View File
@@ -1,50 +0,0 @@
@import './keyframes.css';
.animate-fade-in {
animation: fade-in 0.4s ease-out;
}
.animate-slide-up {
animation: slide-up 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.animate-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}
.animate-spin-slow {
animation: spin-slow 3s linear infinite;
}
.animate-spin-reverse {
animation: spin-reverse 2s linear infinite;
}
.animate-bounce-subtle {
animation: bounce-subtle 2s ease-in-out infinite;
}
.animate-shimmer {
animation: shimmer 2s infinite;
}
.animate-scale-in {
animation: scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.animate-scale-out {
animation: scale-out 0.2s ease-out forwards;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-gradient-x {
background-size: 200% 200%;
animation: gradient-x 3s ease infinite;
}
@@ -1,112 +0,0 @@
import { useEffect } from 'react';
interface UseKeyboardShortcutsProps {
videoRef: React.RefObject<HTMLVideoElement>;
isPlaying: boolean;
volume: number;
isPiPSupported: boolean;
togglePlay: () => void;
toggleMute: () => void;
toggleFullscreen: () => void;
togglePictureInPicture: () => void;
skipForward: () => void;
skipBackward: () => void;
showVolumeBarTemporarily: () => void;
setShowControls: (show: boolean) => void;
setVolume: (volume: number) => void;
setIsMuted: (muted: boolean) => void;
controlsTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>;
}
export function useKeyboardShortcuts({
videoRef,
isPlaying,
volume,
isPiPSupported,
togglePlay,
toggleMute,
toggleFullscreen,
togglePictureInPicture,
skipForward,
skipBackward,
showVolumeBarTemporarily,
setShowControls,
setVolume,
setIsMuted,
controlsTimeoutRef
}: UseKeyboardShortcutsProps) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
const shortcuts = [' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'f', 'F', 'm', 'M', 'i', 'I', '<', '>', ',', '.'];
if (shortcuts.includes(e.key)) {
e.preventDefault();
setShowControls(true);
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
if (isPlaying) {
controlsTimeoutRef.current = setTimeout(() => setShowControls(false), 3000);
}
}
switch (e.key) {
case ' ':
togglePlay();
break;
case 'ArrowLeft':
case '<':
case ',':
skipBackward();
break;
case 'ArrowRight':
case '>':
case '.':
skipForward();
break;
case 'm':
case 'M':
toggleMute();
showVolumeBarTemporarily();
break;
case 'ArrowUp':
if (videoRef.current) {
const newVolume = Math.min(1, volume + 0.05);
setVolume(newVolume);
videoRef.current.volume = newVolume;
setIsMuted(newVolume === 0);
showVolumeBarTemporarily();
}
break;
case 'ArrowDown':
if (videoRef.current) {
const newVolume = Math.max(0, volume - 0.05);
setVolume(newVolume);
videoRef.current.volume = newVolume;
setIsMuted(newVolume === 0);
showVolumeBarTemporarily();
}
break;
case 'f':
case 'F':
toggleFullscreen();
break;
case 'i':
case 'I':
if (isPiPSupported) {
togglePictureInPicture();
}
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isPlaying, volume, isPiPSupported, togglePlay, toggleMute, toggleFullscreen, togglePictureInPicture, skipForward, skipBackward, showVolumeBarTemporarily, setShowControls, controlsTimeoutRef, videoRef, setVolume, setIsMuted]);
}
+10 -11
View File
@@ -8,9 +8,9 @@ interface BadgeProps {
iconPosition?: 'left' | 'right';
}
const BadgeComponent = memo(function Badge({
children,
variant = 'primary',
const BadgeComponent = memo(function Badge({
children,
variant = 'primary',
className = '',
icon,
iconPosition = 'left'
@@ -21,12 +21,11 @@ const BadgeComponent = memo(function Badge({
};
const iconElement = icon && (
<span
className={`inline-flex items-center justify-center ${
iconPosition === 'left' ? 'mr-1' : 'ml-1'
}`}
style={{
width: '0.875em',
<span
className={`inline-flex items-center justify-center ${iconPosition === 'left' ? 'mr-1' : 'ml-1'
}`}
style={{
width: '0.875em',
height: '0.875em',
transform: 'translateZ(0)',
willChange: 'auto',
@@ -37,7 +36,7 @@ const BadgeComponent = memo(function Badge({
);
return (
<span
<span
className={`
inline-flex items-center justify-center
px-1.5 py-0.5
@@ -60,5 +59,5 @@ const BadgeComponent = memo(function Badge({
// Export both named and default for compatibility
export const Badge = BadgeComponent;
export { BadgeComponent as default };
-7
View File
@@ -1,10 +1,3 @@
export type { IconProps } from './icons/types.tsx';
export { MediaIcons } from './icons/media-icons';
export { NavigationIcons } from './icons/navigation-icons';
export { UtilityIcons } from './icons/utility-icons';
// For backward compatibility, export a combined Icons object
import { MediaIcons } from './icons/media-icons';
import { NavigationIcons } from './icons/navigation-icons';
+1 -1
View File
@@ -3,7 +3,7 @@
* Handles parallel requests and data normalization
*/
export { searchVideos, searchVideosBySource } from './search-api';
export { searchVideos } from './search-api';
export { getVideoDetail } from './detail-api';
+2 -2
View File
@@ -4,11 +4,10 @@ import type {
ApiSearchResponse,
} from '@/lib/types';
import { fetchWithTimeout, withRetry } from './http-utils';
/**
* Search videos from a single source
*/
export async function searchVideosBySource(
async function searchVideosBySource(
query: string,
source: VideoSource,
page: number = 1
@@ -64,6 +63,7 @@ export async function searchVideosBySource(
}
}
/**
* Search videos from multiple sources in parallel
*/
+1 -2
View File
@@ -13,5 +13,4 @@ export function getSourceById(id: string): VideoSource | undefined {
return DEFAULT_SOURCES.find(source => source.id === id);
}
// Re-export DEFAULT_SOURCES for backward compatibility
export { DEFAULT_SOURCES };
+1 -1
View File
@@ -7,7 +7,7 @@ import type { Video, SourceBadge } from '@/lib/types';
import { useSearchState } from './useSearchState';
import { useSearchAction } from './useSearchAction';
export interface ParallelSearchResult {
interface ParallelSearchResult {
loading: boolean;
results: Video[];
availableSources: SourceBadge[];
+4 -2
View File
@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from 'react';
export interface VideoData {
interface VideoData {
vod_id: string;
vod_name: string;
vod_pic?: string;
@@ -15,7 +15,7 @@ export interface VideoData {
episodes?: Array<{ name?: string; url: string }>;
}
export interface UseVideoPlayerReturn {
interface UseVideoPlayerReturn {
videoData: VideoData | null;
loading: boolean;
videoError: string;
@@ -27,6 +27,8 @@ export interface UseVideoPlayerReturn {
fetchVideoDetails: () => Promise<void>;
}
export function useVideoPlayer(
videoId: string | null,
source: string | null,
+1 -1
View File
@@ -15,7 +15,7 @@ export type SortOption =
| 'name-asc'
| 'name-desc';
export interface AppSettings {
interface AppSettings {
sources: VideoSource[];
sortBy: SortOption;
searchHistory: boolean;
+4 -4
View File
@@ -3,7 +3,7 @@
* Following Liquid Glass design system principles
*/
export interface LatencyInfo {
interface LatencyInfo {
value: number;
label: string;
color: string;
@@ -41,13 +41,13 @@ export function getLatencyInfo(latency: number): LatencyInfo {
};
}
/**
* Format latency for display
* @param latency - Response time in milliseconds
* @returns Formatted string in milliseconds (e.g., "345ms", "1240ms")
*/
export function formatLatency(latency: number): string {
function formatLatency(latency: number): string {
return `${latency}ms`;
}
+1 -1
View File
@@ -35,7 +35,7 @@ export async function processSearchStream({
timeoutId = setTimeout(() => {
if (!isCompleted) {
console.log('Search timeout: No progress for 3 seconds, auto-completing');
isCompleted = true;
onComplete();
}
+1 -35
View File
@@ -9,8 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@vercel/analytics": "^1.5.0",
"artplayer": "^5.1.7",
"hls.js": "^1.5.15",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
@@ -23,6 +21,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"postcss": "^8.5.6",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -2420,15 +2419,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/artplayer": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/artplayer/-/artplayer-5.3.0.tgz",
"integrity": "sha512-yExO39MpEg4P+bxgChxx1eJfiUPE4q1QQRLCmqGhlsj+ANuaoEkR8hF93LdI5ZyrAcIbJkuEndxEiUoKobifDw==",
"license": "MIT",
"dependencies": {
"option-validator": "^2.0.6"
}
},
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -3981,12 +3971,6 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/hls.js": {
"version": "1.6.14",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz",
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
"license": "Apache-2.0"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4589,15 +4573,6 @@
"json-buffer": "3.0.1"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -5264,15 +5239,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/option-validator": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/option-validator/-/option-validator-2.0.6.tgz",
"integrity": "sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==",
"license": "MIT",
"dependencies": {
"kind-of": "^6.0.3"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+1 -2
View File
@@ -10,8 +10,6 @@
},
"dependencies": {
"@vercel/analytics": "^1.5.0",
"artplayer": "^5.1.7",
"hls.js": "^1.5.15",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
@@ -24,6 +22,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"postcss": "^8.5.6",
"tailwindcss": "^4",
"typescript": "^5"
}
-37
View File
@@ -1,37 +0,0 @@
#!/bin/bash
# KVideo Platform - Installation Script
echo "🎬 KVideo Platform Setup"
echo "========================"
echo ""
# Check if npm is installed
if ! command -v npm &> /dev/null; then
echo "❌ Error: npm is not installed"
echo "Please install Node.js and npm first: https://nodejs.org/"
exit 1
fi
echo "📦 Installing dependencies..."
npm install
if [ $? -eq 0 ]; then
echo "✅ Dependencies installed successfully!"
else
echo "❌ Failed to install dependencies"
exit 1
fi
echo ""
echo "📝 Next steps:"
echo "1. Configure video API sources in lib/api/video-sources.ts"
echo "2. Run 'npm run dev' to start development server"
echo "3. Visit http://localhost:3000"
echo ""
echo "📖 Documentation:"
echo "- SETUP.md for detailed setup instructions"
echo "- IMPLEMENTATION.md for architecture details"
echo "- SUMMARY.md for overview"
echo ""
echo "✨ Setup complete! Happy coding!"