# 文件下载 — 架构文档

> 对应 Gitea issues #14–#18
> 参考实现：`im-client-ios-swift-demo` FileDownloadManager + AudioPlaybackService + ChatMessageBubble

---

## 1. 功能范围

| Issue | 功能 | 状态 |
|-------|------|------|
| #14 | CDN URL 解析器（CdnUrlResolver） | ✅ 已实现 |
| #15 | FileDownloadManager — 下载服务 + 缓存 | ✅ 已实现 |
| #16 | FileMessageBubble — 文件气泡三态 UI | ✅ 已实现 |
| #17 | AudioMessageBubble — 语音下载框架 | ✅ 框架（播放需 audioplayers） |
| #18 | VideoMessageBubble — 视频缩略图框架 | ✅ 框架（播放需 video_player） |

---

## 2. 目录结构

```
core/services/
├── cdn_url_resolver.dart          # CDN 路径 → 完整 URL
└── file_download_manager.dart     # 下载服务（进度追踪 + 缓存）

features/chat/view/widgets/
├── file_message_bubble.dart       # 文件消息气泡（三态 UI）
├── audio_message_bubble.dart      # 语音消息气泡（下载框架）
└── video_message_bubble.dart      # 视频消息气泡（缩略图框架）
```

---

## 3. 核心概念

### 3.1 FileDownloadState

```
idle          → 未下载（显示下载图标）
downloading(progress: 0.0~1.0) → 下载中（进度条）
downloaded(localPath: String)  → 已缓存（显示打开图标）
failed(error: String)         → 失败（显示重试图标）
```

### 3.2 CDN URL 路由策略

```
相对路径   Files/xxx.pdf     → apiBaseUrl/Files/xxx.pdf
完整 HTTP  http://xxx.com/f  → 直接使用
S3 URL     *.amazonaws.com   → apiBaseUrl + S3 path（通过 API 代理）
```

S3 桶是私有的，直接访问返回 403。所有下载都必须经过 `apiBaseUrl` 代理。

### 3.3 缓存策略

```
缓存目录：Directory.systemTemp/im_file_cache/
文件名：  {url.hashCode 8位 hex}_{sanitized_filename}
命中条件：文件存在 && 大小 > 0
清理策略：由 OS 自动清理 temp 目录（同 iOS Library/Caches 语义）
```

---

## 4. 数据流

### 4.1 文件消息下载

```
FileMessageBubble.onTap
  └─ fileDownloadManagerProvider.notifier.download(url, fileName)
       ├─ CdnUrlResolver.resolve(url)          → 完整下载 URL
       ├─ _cacheKeyFor(url, fileName)          → 本地文件名
       ├─ File(cachePath).exists() ?
       │    true  → state = downloaded(cachePath)
       │    false → NetworksSdkApi.executeDownload(url, savePath, onProgress)
       │              → state = downloading(progress)
       │              → state = downloaded(cachePath) / failed(error)
       └─ 返回 localPath（供 open_filex 打开）
```

### 4.2 语音消息

```
AudioMessageBubble
  └─ 点击播放按钮
       └─ FileDownloadManager.download(voiceUrl, 'voice_{cmid}.m4a')
            → downloaded(localPath)
            → TODO: AudioPlayer().play(DeviceFileSource(localPath))
```

### 4.3 视频消息

```
VideoMessageBubble
  └─ 显示：Image.network(CdnUrlResolver.resolve(thumbUrl))  // 缩略图
  └─ 点击播放按钮
       → TODO: Navigator 推送全屏视频页（video_player 流式播放）
```

---

## 5. Provider 设计

```
fileDownloadManagerProvider
  NotifierProvider<FileDownloadManager, Map<String, FileDownloadState>>
    └─ NetworksSdkApi (via networkSdkApiProvider)
```

与 Settings ViewModel 一致，使用手动 `Notifier<T>` + `NotifierProvider`，
不依赖 `@riverpod` 代码生成。

---

## 6. 消息类型路由（rawContent 解析）

| typ | 类型 | rawContent 格式 |
|-----|------|----------------|
| 3   | 语音 | `{"url":"...","duration":N}` |
| 4   | 视频 | `{"url":"...","thumb":"...","size":N}` |
| 6   | 文件 | `{"url":"...","name":"...","size":N}` |
| 24  | 视频（秘密） | 同 typ=4 |

---

## 7. 待完成

- **audioplayers 接入**：`AudioMessageBubble` 中 TODO 解开，安装 `audioplayers: ^6.0.0`
- **video_player 接入**：`VideoMessageBubble` 点击推送全屏路由，安装 `video_player: ^2.9.0`
- **open_filex 接入**：`FileMessageBubble` downloaded 态打开文件，安装 `open_filex: ^3.0.3`
- **XOR 解密**：`secret/` 路径文件解密（cipher_guard_sdk 接入时实现）

### 接入 open_filex 步骤

```yaml
# apps/im_app/pubspec.yaml
dependencies:
  open_filex: ^3.0.3
```

```dart
// FileMessageBubble downloaded 态 onTap：
import 'package:open_filex/open_filex.dart';
await OpenFilex.open(localPath);
```

### 接入 audioplayers 步骤

```yaml
# apps/im_app/pubspec.yaml
dependencies:
  audioplayers: ^6.0.0
```

```dart
// AudioMessageBubble 播放：
import 'package:audioplayers/audioplayers.dart';
final player = AudioPlayer();
await player.play(DeviceFileSource(localPath));
```
