mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-15 00:33:44 +08:00
refactor: Remove player-specific dependencies, setup script, keyboard shortcuts, and project documentation.
This commit is contained in:
-774
@@ -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>
|
||||
@@ -1,410 +0,0 @@
|
||||
# KVideo
|
||||
|
||||
> 基于 Liquid Glass 设计理念的现代化流媒体视频聚合平台
|
||||
|
||||
[](https://nextjs.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://tailwindcss.com/)
|
||||
[](LICENSE)
|
||||
|
||||
## 目录
|
||||
|
||||
- [关于项目](#关于项目)
|
||||
- [核心特性](#核心特性)
|
||||
- [技术栈](#技术栈)
|
||||
- [设计理念](#设计理念)
|
||||
- [快速开始](#快速开始)
|
||||
- [系统要求](#系统要求)
|
||||
- [安装步骤](#安装步骤)
|
||||
- [使用指南](#使用指南)
|
||||
- [开发模式](#开发模式)
|
||||
- [生产构建](#生产构建)
|
||||
- [核心功能](#核心功能)
|
||||
- [智能并行搜索](#智能并行搜索)
|
||||
- [实时流式传输](#实时流式传输)
|
||||
- [源可用性检测](#源可用性检测)
|
||||
- [观看历史管理](#观看历史管理)
|
||||
- [自适应视频播放器](#自适应视频播放器)
|
||||
- [项目架构](#项目架构)
|
||||
- [贡献指南](#贡献指南)
|
||||
- [许可证](#许可证)
|
||||
- [联系方式](#联系方式)
|
||||
|
||||
## 关于项目
|
||||
|
||||
KVideo 是一个现代化的流媒体视频聚合平台,采用 **Liquid Glass** 设计系统打造极致的用户体验。平台通过智能并行搜索技术,实时聚合多个视频源的内容,为用户提供流畅、直观且视觉震撼的观影体验。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 🔍 **智能并行搜索** - 同时查询 15+ 视频源,实时流式返回结果
|
||||
- 🎬 **自适应播放器** - 支持 HLS/M3U8 流,自动源切换,断点续播
|
||||
- 🎨 **Liquid Glass UI** - 毛玻璃效果,流体动画,深浅色主题无缝切换
|
||||
- ⚡ **实时可用性检测** - 搜索时自动过滤失效源,确保播放成功率
|
||||
- 📊 **类型智能筛选** - 自动识别分类(电影/剧集/综艺),支持多选过滤
|
||||
- 📚 **观看历史追踪** - 自动记录播放进度,快速恢复观看
|
||||
- 🌐 **无服务器架构** - 基于 Next.js App Router,API 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>
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,7 @@ export type SortOption =
|
||||
| 'name-asc'
|
||||
| 'name-desc';
|
||||
|
||||
export interface AppSettings {
|
||||
interface AppSettings {
|
||||
sources: VideoSource[];
|
||||
sortBy: SortOption;
|
||||
searchHistory: boolean;
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Generated
+1
-35
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
Reference in New Issue
Block a user