# 工具函数

## 工具函数概览

OFPlayer 的工具函数位于 `src/utils/` 目录，提供通用的辅助功能。

## 工具列表

### versionedStorage

**位置**: `src/utils/versionedStorage.js`

**职责**: 构建 ID 感知的 localStorage 封装，自动失效过期数据

**原理**:
- 每次构建时生成唯一的 `__BUILD_ID__`
- 存储数据时附带构建 ID
- 读取时检查构建 ID，不匹配则返回 null

**API**:

```javascript
import { readVersioned, writeVersioned, removeVersioned } from '../utils/versionedStorage'

// 写入版本化数据
writeVersioned('ofp:visual', {
  theme: 'mist',
  colorScheme: 'system',
  motion: 'full',
})

// 读取版本化数据（构建 ID 不匹配返回 null）
const data = readVersioned('ofp:visual')

// 删除版本化数据
removeVersioned('ofp:visual')
```

**使用场景**:
- 主题/颜色方案/动画级别的快速应用（避免首次加载闪烁）
- 用户偏好设置的本地缓存
- 任何需要在部署后自动失效的数据

**存储格式**:

```javascript
{
  __BUILD_ID__: 'abc123',  // 构建时注入
  data: { ... }            // 实际数据
}
```

---

### lrcParser

**位置**: `src/utils/lrcParser.js`

**职责**: 解析 LRC 格式的歌词文件

**LRC 格式**:

```
[ti:Song Title]
[ar:Artist]
[al:Album]
[00:00.00]First line of lyrics
[00:05.50]Second line of lyrics
[00:12.30]Third line of lyrics
```

**API**:

```javascript
import { parseLrc } from '../utils/lrcParser'

const lrcContent = `
[00:00.00]Hello
[00:05.50]World
`

const result = parseLrc(lrcContent)

// result 结构:
// {
//   metadata: {
//     ti: 'Song Title',
//     ar: 'Artist',
//     al: 'Album',
//   },
//   lines: [
//     { time: 0, text: 'Hello' },
//     { time: 5.5, text: 'World' },
//   ]
// }
```

**时间标签格式**:
- `[mm:ss.xx]` - 分钟:秒.毫秒
- `[mm:ss.xxx]` - 分钟:秒.毫秒（三位）
- `[mm:ss]` - 分钟:秒

---

### colorExtractor

**位置**: `src/utils/colorExtractor.js`

**职责**: 从专辑封面提取主色调

**API**:

```javascript
import { extractDominantColor } from '../utils/colorExtractor'

// 从图片 URL 提取主色调
const color = await extractDominantColor(imageUrl)

// 返回格式:
// {
//   r: 128,
//   g: 64,
//   b: 32,
//   hex: '#804020',
//   hsl: 'hsl(20, 60%, 31%)',
// }
```

**使用场景**:
- 沉浸式播放视图的背景色
- 动态主题色

---

### zipBuilder

**位置**: `src/utils/zipBuilder.js`

**职责**: 构建 ZIP 文件用于库导出

**API**:

```javascript
import { buildZip, downloadBlob } from '../utils/zipBuilder'

// 构建 ZIP
const entries = [
  { name: 'song1.mp3', blob: blob1 },
  { name: 'song2.mp3', blob: blob2 },
]

const zipBlob = await buildZip(entries, {
  onProgress: (done, total) => {
    console.log(`${done}/${total}`)
  },
})

// 下载 ZIP
downloadBlob(zipBlob, 'my-music.zip')
```

## Composables

### useAudioPlayer

**位置**: `src/composables/useAudioPlayer.js`

**职责**: 封装 HTMLAudioElement 的播放控制

**API**:

```javascript
import { useAudioPlayer } from '../composables/useAudioPlayer'

const {
  status,        // Ref<string>: 'idle' | 'loading' | 'ready' | 'playing' | 'paused'
  currentTime,   // Ref<number>: 当前播放位置
  duration,      // Ref<number>: 曲目时长
  volume,        // Ref<number>: 音量
  isPlaying,     // Ref<boolean>: 是否正在播放
  error,         // Ref<Error>: 错误信息
  activeTrackId, // Ref<string>: 当前曲目 ID
  
  loadTrack,     // (track, options) => Promise<boolean>
  play,          // () => Promise<boolean>
  pause,         // () => void
  seek,          // (time) => void
  setVolume,     // (volume) => void
  reset,         // () => void
  dispose,       // () => void
} = useAudioPlayer({
  initialVolume: 0.8,
  onPlay: () => {},
  onPause: () => {},
  onSeek: () => {},
  onTrackEnded: ({ trackId }) => {},
  onTrackDurationChange: ({ trackId, duration }) => {},
})
```

---

### useI18n

**位置**: `src/composables/useI18n.js`

**职责**: 国际化支持

**API**:

```javascript
import { useI18n } from '../composables/useI18n'

const { locale, t } = useI18n()

// 获取当前语言
locale.value  // 'zh-CN' 或 'en'

// 翻译文本
t('player.play')           // '播放' 或 'Play'
t('player.searchPlaceholder') // '搜索曲目...' 或 'Search tracks...'
```

**支持的语言**:
- `zh-CN` - 简体中文
- `en` - 英文

---

### useSortOptions

**位置**: `src/composables/useSortOptions.js`

**职责**: 排序选项定义

**API**:

```javascript
import { useSortOptions } from '../composables/useSortOptions'

const { sortOptions, applySortToTracks } = useSortOptions(t, locale)

// 排序选项列表
sortOptions.value
// [
//   { value: 'recent', label: '最近导入' },
//   { value: 'title', label: '标题' },
//   { value: 'artist', label: '艺术家' },
//   { value: 'album', label: '专辑' },
//   { value: 'duration', label: '时长' },
// ]

// 应用排序
const sorted = applySortToTracks(tracks, 'title')
```

---

### useEntitlement

**位置**: `src/composables/useEntitlement.js`

**职责**: 功能权限检查

---

### useScrollReveal

**位置**: `src/composables/useScrollReveal.js`

**职责**: 滚动显示动画

---

### useNoteCanvas

**位置**: `src/composables/useNoteCanvas.js`

**职责**: Canvas 绘制（音符动画）

---

### useDampedSectionScroll

**位置**: `src/composables/useDampedSectionScroll.js`

**职责**: 阻尼滚动行为

## 图标库

### lucide.js

**位置**: `src/lib/lucide.js`

**职责**: 树摇优化的 lucide 图标导入

**说明**: 
- 仅导入使用的图标（约 46 个）
- 避免导入整个 lucide 库（约 1.2MB）

**使用方式**:

```javascript
import { Play, Pause, SkipForward } from 'lucide-vue-next'

// 在 Vue 模板中使用
<Play />
<Pause />
<SkipForward />
```

**已导入的图标**:

```
ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
Play, Pause, SkipBack, SkipForward,
Heart, MoreHorizontal, MoreVertical,
Search, X, Plus, Minus,
Settings, Settings2, Menu, 
Volume, Volume1, Volume2, VolumeX,
Disc, Disc2, Disc3, Music, ListMusic,
HardDriveDownload, Upload, Download,
Globe, ExternalLink, Link,
PanelRightClose, PanelRightOpen,
Maximize, Minimize, Expand, Shrink,
Check, AlertCircle, Info, HelpCircle,
Clock, Calendar, User, Users,
File, FileAudio, Folder, FolderOpen,
Star, Bookmark, Tag,
Edit, Trash, Copy, Move,
RefreshCw, RotateCcw, RotateCw,
Sun, Moon, Monitor, Eye, EyeOff,
ArrowUp, ArrowDown, ArrowLeft, ArrowRight,
ChevronsUpDown, GripVertical
```

## 使用示例

### 完整的文件导入流程

```javascript
import { isSupportedAudioFile } from '../models/track'
import { extractMetadata } from '../services/metadataService'
import { createTrackModel } from '../models/track'

async function importAudioFile(file, libraryId) {
  // 1. 检查文件类型
  if (!isSupportedAudioFile(file)) {
    throw new Error('Unsupported audio format')
  }
  
  // 2. 提取元数据
  const metadata = await extractMetadata(file)
  
  // 3. 创建曲目模型
  const track = createTrackModel(file, {
    libraryId,
    ...metadata,
  })
  
  return track
}
```

### 主题色提取

```javascript
import { extractDominantColor } from '../utils/colorExtractor'

async function updatePlayerTheme(artworkUrl) {
  try {
    const color = await extractDominantColor(artworkUrl)
    document.documentElement.style.setProperty('--dynamic-color', color.hex)
  } catch (error) {
    console.error('Failed to extract color:', error)
  }
}
```

### 导出库为 ZIP

```javascript
import { buildZip, downloadBlob } from '../utils/zipBuilder'

async function exportLibrary(dataService) {
  const assets = await dataService.catalog.getAllTrackAssets()
  const validAssets = assets.filter(a => a.assetBlob instanceof Blob)
  
  const entries = validAssets.map((asset, index) => ({
    name: `${index + 1}_${asset.fileName}`,
    blob: asset.assetBlob,
  }))
  
  const zip = await buildZip(entries, {
    onProgress: (done, total) => {
      console.log(`Progress: ${done}/${total}`)
    },
  })
  
  downloadBlob(zip, `library-export-${Date.now()}.zip`)
}
```

## 最佳实践

### 1. 使用版本化存储

```javascript
// ✓ 正确 - 使用 versionedStorage
writeVersioned('my-key', data)
const data = readVersioned('my-key')

// ✗ 错误 - 直接使用 localStorage
localStorage.setItem('my-key', JSON.stringify(data))
// 可能在部署后读取到过期数据
```

### 2. 处理异步操作

```javascript
// ✓ 正确 - 使用 async/await
async function extractColor(url) {
  try {
    return await extractDominantColor(url)
  } catch (error) {
    console.error('Color extraction failed:', error)
    return null
  }
}

// ✗ 错误 - 忽略 Promise
function extractColor(url) {
  return extractDominantColor(url)  // 可能失败
}
```

### 3. 释放资源

```javascript
// ✓ 正确 - 释放 Object URL
URL.revokeObjectURL(objectUrl)

// ✓ 正确 - 使用模型的释放函数
revokeTrackResource(track)
```
