# 状态管理

## 状态管理概览

OFPlayer 使用 Vue 3 Composition API 的 `ref` 和 `computed` 进行状态管理，不依赖 Vuex 或 Pinia。每个 Store 是一个工厂函数，返回响应式状态和操作方法。

## Store 设计原则

1. **独立性** - 每个 Store 管理特定领域的状态
2. **工厂模式** - 通过工厂函数创建，支持依赖注入
3. **响应式** - 使用 Vue 的响应式系统
4. **可组合** - Store 之间可以组合使用

## 核心 Store

### libraryStore

**位置**: `src/stores/libraryStore.js`

**职责**: 管理库、播放列表、曲目及其关系

**状态**:

| 状态 | 类型 | 说明 |
|------|------|------|
| `libraries` | `Ref<Array>` | 库列表 |
| `playlists` | `Ref<Array>` | 播放列表 |
| `tracks` | `Ref<Array>` | 曲目列表 |
| `playlistTrackRelations` | `Ref<Array>` | 播放列表-曲目关系 |

**计算属性**:

| 属性 | 类型 | 说明 |
|------|------|------|
| `libraryIds` | `Computed<string[]>` | 库 ID 列表 |
| `trackIds` | `Computed<string[]>` | 曲目 ID 列表 |
| `trackCount` | `Computed<number>` | 曲目数量 |
| `hasTracks` | `Computed<boolean>` | 是否有曲目 |

**查找表（O(1) 复杂度）**:

| 属性 | 类型 | 说明 |
|------|------|------|
| `tracksById` | `Computed<Map>` | ID 到曲目的映射 |
| `librariesById` | `Computed<Map>` | ID 到库的映射 |
| `playlistsById` | `Computed<Map>` | ID 到播放列表的映射 |

**方法**:

```javascript
// 初始化/刷新
hydrate()

// 查找
getLibraryById(libraryId)
getPlaylistById(playlistId)
getTrackById(trackId)
getTracksForLibrary(libraryId)
getPlaylistTrackRelations(playlistId)
getDefaultPlaylistForLibrary(libraryId)
getDefaultCollectionRef(libraryId)
isCollectionAvailableForLibrary(libraryId, collectionRef)

// 库操作
createLibrary(name)
renameLibrary(libraryId, name)
deleteLibrary(libraryId)
reorderLibraries(orderedLibraryIds)

// 播放列表操作
createPlaylist({ libraryId, name })
renamePlaylist(playlistId, name)
deletePlaylist(playlistId)
reorderPlaylists({ libraryId, orderedPlaylistIds })

// 曲目操作
importFiles({ libraryId, files })
updateTrackMetadata(trackId, patch)
addTrackToPlaylist({ playlistId, trackId, index })
removeTrackFromPlaylist({ playlistId, trackId })
deleteTrackFromLibrary(trackId)
toggleFavorite(trackId)
setFavorite(trackId, isFavorite)
reorderPlaylistTracks({ playlistId, orderedTrackIds })

// 清理
dispose()
```

**使用示例**:

```javascript
const libraryStore = createLibraryStore({
  libraryService,
  playlistService,
  trackService,
})

// 获取所有库
const libraries = libraryStore.libraries

// 创建新库
const { library, defaultPlaylist } = await libraryStore.createLibrary('My Music')

// 查找曲目
const track = libraryStore.getTrackById(trackId)
```

---

### playerStore

**位置**: `src/stores/playerStore.js`

**职责**: 管理播放状态、当前曲目、播放历史

**状态**:

| 状态 | 类型 | 说明 |
|------|------|------|
| `activeTrack` | `Ref<Object>` | 当前活跃曲目 |
| `recentHistory` | `Ref<Array>` | 最近播放历史 |

**从 audioPlayer 继承的状态**:

| 状态 | 类型 | 说明 |
|------|------|------|
| `status` | `Ref<string>` | 播放状态 |
| `currentTime` | `Ref<number>` | 当前播放位置 |
| `duration` | `Ref<number>` | 曲目时长 |
| `volume` | `Ref<number>` | 音量 |
| `activeTrackId` | `Ref<string>` | 当前曲目 ID |
| `isPlaying` | `Ref<boolean>` | 是否正在播放 |
| `error` | `Ref<Error>` | 错误信息 |

**方法**:

```javascript
// 初始化
hydrate()

// 播放控制
loadTrack(track, loadOptions)
play()
pause()
toggle()
seek(nextTime)
setVolume(nextVolume)

// 状态管理
reset()
dispose()
```

**播放历史记录**:

```javascript
{
  trackId: string,
  type: 'played' | 'paused' | 'ended',
  position: number,      // 播放位置
  duration: number,      // 曲目时长
  recordedAt: string     // ISO 时间戳
}
```

**历史限制**: 最多保存 100 条记录

**使用示例**:

```javascript
const playerStore = createPlayerStore({
  dataService,
  initialVolume: 0.8,
  onTrackEnded: () => playNext(),
  onTrackDurationChange: ({ trackId, duration }) => {
    libraryStore.updateTrackMetadata(trackId, { duration })
  },
})

// 播放曲目
await playerStore.loadTrack(track, { autoplay: true })

// 暂停
playerStore.pause()

// 跳转
playerStore.seek(30)  // 跳转到 30 秒
```

---

### preferencesStore

**位置**: `src/stores/preferencesStore.js`

**职责**: 管理用户偏好设置

**状态**:

| 状态 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `volume` | `number` | `0.8` | 音量 |
| `rememberVolume` | `boolean` | `false` | 是否记住音量 |
| `language` | `string` | `'zh-CN'` | 语言 |
| `theme` | `string` | `'mist'` | 主题 |
| `colorScheme` | `string` | `'system'` | 颜色方案 |
| `motion` | `string` | `'full'` | 动画级别 |
| `showTechnicalMetadata` | `boolean` | `true` | 显示技术元数据 |
| `librarySearchQuery` | `string` | `''` | 搜索查询 |
| `librarySortOption` | `string` | `'recent'` | 排序选项 |
| `libraryTypeFilter` | `string` | `'all'` | 类型过滤 |
| `activeLibrary` | `string` | `null` | 当前活跃库 |
| `activeCollection` | `string` | `null` | 当前活跃集合 |
| `sidebarSection` | `string` | `null` | 侧边栏展开部分 |
| `telemetryEnabled` | `boolean` | `null` | 遥测同意状态 |

**方法**:

```javascript
// 初始化
hydrate()

// 设置控制
openSettings()
closeSettings()
toggleSettings()

// 偏好设置修改
setVolume(volume)
setRememberVolume(rememberVolume)
setLanguage(language)
setTheme(theme)
setColorScheme(colorScheme)
setMotion(motion)
setShowTechnicalMetadata(showTechnicalMetadata)
setSearchQuery(query)
setSortOption(option)
setTypeFilter(option)
setActiveLibrary(activeLibrary)
setActiveCollection(activeCollection)
setSidebarSection(sidebarSection)
setTelemetryConsent(value)
```

**持久化机制**:
- 使用 `versionedStorage` 保存视觉偏好（主题、颜色方案、动画）
- 使用 `dataService.preferences` 保存所有偏好
- 视觉偏好同步到 `document.documentElement.dataset`

**使用示例**:

```javascript
const preferencesStore = createPreferencesStore({ dataService })

// 切换主题
preferencesStore.setTheme('paper')

// 设置语言
preferencesStore.setLanguage('en')

// 获取当前音量
const volume = preferencesStore.volume.value
```

---

### sessionStore

**位置**: `src/stores/sessionStore.js`

**职责**: 管理会话状态、播放队列

**状态**:

| 状态 | 类型 | 说明 |
|------|------|------|
| `queueTrackIds` | `Ref<Array>` | 队列中的曲目 ID |
| `currentTrackId` | `Ref<string>` | 当前曲目 ID |

**方法**:

```javascript
// 初始化
hydrate()

// 队列管理
setQueue(trackIds)
setCurrentTrack(trackId)
getNextTrackId()
getPreviousTrackId()
```

**使用示例**:

```javascript
const sessionStore = createSessionStore({ dataService })

// 设置队列
sessionStore.setQueue(['track1', 'track2', 'track3'])

// 获取下一首
const nextId = sessionStore.getNextTrackId()
```

---

### entitlementStore

**位置**: `src/stores/entitlementStore.js`

**职责**: 管理功能权限（Pro 功能门控）

**状态**:

| 状态 | 类型 | 说明 |
|------|------|------|
| `plan` | `Ref<string>` | 当前套餐 |
| `features` | `Ref<Object>` | 功能开关 |

---

### uiStore

**位置**: `src/stores/uiStore.js`

**职责**: 管理 UI 状态

## Store 之间的协作

### 协作示例：播放曲目

```javascript
// ofplayerApp.js 中的 selectTrack 实现
async function selectTrack(trackId, options = {}) {
  const autoplay = options.autoplay !== false
  const track = libraryStore.getTrackById(trackId)  // 从 libraryStore 获取曲目

  if (!track) return false

  sessionStore.setCurrentTrack(trackId)  // 更新 sessionStore
  return hydrateTrack(trackId, { autoplay })  // 更新 playerStore
}
```

### 协作示例：导入文件

```javascript
async function importFiles(files) {
  const activeLibraryId = ensureActiveLibrarySelection()  // 使用 preferencesStore

  if (!activeLibraryId) return []

  const importedTracks = await libraryStore.importFiles({  // 更新 libraryStore
    libraryId: activeLibraryId,
    files,
  })

  syncQueueWithCatalog()  // 同步 sessionStore

  if (!sessionStore.currentTrackId.value) {
    sessionStore.setCurrentTrack(importedTracks[0].id)
    await hydrateTrack(importedTracks[0].id, { autoplay: false })
  }

  return importedTracks
}
```

## 数据同步机制

### 队列同步

```javascript
function syncQueueWithCatalog() {
  // 构建新队列：保留原有顺序，补充新曲目
  const nextQueue = buildQueueFromCatalog(
    sessionStore.queueTrackIds.value,
    libraryStore.trackIds.value,
  )

  sessionStore.setQueue(nextQueue)

  // 确保当前曲目有效
  if (sessionStore.currentTrackId.value && 
      libraryStore.getTrackById(sessionStore.currentTrackId.value)) {
    return nextQueue
  }

  // 如果当前曲目无效，选择第一首
  sessionStore.setCurrentTrack(nextQueue[0] ?? null)
  return nextQueue
}
```

### 库选择同步

```javascript
function ensureActiveLibrarySelection() {
  const activeLibraryId = preferencesStore.activeLibrary.value
  const resolvedLibrary =
    libraryStore.getLibraryById(activeLibraryId) ?? 
    libraryStore.libraries.value[0] ?? 
    null

  if (!resolvedLibrary) return null

  // 确保选择的库存在
  if (resolvedLibrary.id !== activeLibraryId) {
    preferencesStore.setActiveLibrary(resolvedLibrary.id)
  }

  // 确保选择的集合对当前库有效
  const currentCollection = preferencesStore.activeCollection.value
  if (!libraryStore.isCollectionAvailableForLibrary(resolvedLibrary.id, currentCollection)) {
    preferencesStore.setActiveCollection(
      libraryStore.getDefaultCollectionRef(resolvedLibrary.id)
    )
  }

  return resolvedLibrary.id
}
```

## 状态初始化流程

```javascript
// 1. 同步创建 Store 实例
const libraryStore = createLibraryStore({ libraryService, playlistService, trackService })
const sessionStore = createSessionStore({ dataService })
const preferencesStore = createPreferencesStore({ dataService })
const playerStore = createPlayerStore({ dataService, initialVolume: preferencesStore.volume.value })

// 2. Vue 应用挂载（UI 立即可交互）
app.mount('#app')

// 3. 异步水合数据
await Promise.all([
  preferencesStore.hydrate(),
  libraryStore.hydrate(),
  sessionStore.hydrate(),
  playerStore.hydrate(),
])

// 4. 同步状态
playerStore.setVolume(preferencesStore.volume.value)
syncQueueWithCatalog()
ensureActiveLibrarySelection()

// 5. 标记水合完成
isHydrating.value = false
```

## 最佳实践

### 1. 使用 computed 保持响应性

```javascript
// ✓ 正确
const currentTrack = computed(() => libraryStore.getTrackById(currentTrackId.value))

// ✗ 错误 - 不会响应变化
const currentTrack = libraryStore.getTrackById(currentTrackId.value)
```

### 2. 通过 ofplayerApp 访问

```javascript
// ✓ 正确 - 通过应用层访问
const ofplayer = useOFPlayerApp()
ofplayer.selectTrack(trackId)

// ✗ 错误 - 直接访问 Store（可能导致状态不一致）
sessionStore.setCurrentTrack(trackId)
playerStore.loadTrack(track)
```

### 3. 处理异步操作

```javascript
// ✓ 正确 - 使用 async/await
async function handleSelectTrack(trackId) {
  const success = await ofplayer.selectTrack(trackId)
  if (!success) {
    // 处理失败
  }
}

// ✗ 错误 - 忽略 Promise
function handleSelectTrack(trackId) {
  ofplayer.selectTrack(trackId)  // 可能未完成就继续执行
}
```
