December 24, 2025
4 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

十、导出支持自定义文件名

10.1 需求背景

根据项目规范:

“导出文件的文件名必须使用随机生成的八位小写字母,禁止使用明文或可预测的文件名,以增强安全性。”

但我更希望能够自定义文件名,同时默认填充随机名称作为建议。

10.2 实现方案

方案取舱

  1. ✅ 默认用随机名填充输入框,可编辑
  2. ❌ 强制随机名,不允许修改(体验不好)
  3. ❌ 默认空白,使用时必须输入(增加操作步骤)

10.3 代码修改

1. file_utils.dart - 新增函数

/// 生成随机名称(不含扩展名)
/// 返回 8 位小写字母
String generateRandomName() {
  final random = Random.secure();
  return String.fromCharCodes(
    List.generate(8, (_) => random.nextInt(26) + 97), // a-z
  );
}
 
/// 生成随机文件名(复用上面的函数)
String generateRandomFileName(String extension) {
  final name = generateRandomName();
  return '$name.$extension';
}

2. _DownloadJob - 新增字段

class _DownloadJob {
  // ... 现有字段 ...
  
  // 导出相关字段
  final bool isExport;
  final String? exportPassword;
  final String? exportFileName;  // 新增:自定义的导出文件名(不含扩展名)
}

3. enqueueExport - 新增参数

void enqueueExport(FileMetadata file, {required String password, String? fileName}) {
  // ...
  _downloadQueue.add(
    _DownloadJob(
      id: id,
      file: file,
      isExport: true,
      exportPassword: password,
      exportFileName: fileName,  // 传递自定义文件名
    ),
  );
}

4. _doExportByPath - 使用自定义文件名

Future<void> _doExportByPath(_DownloadJob job) async {
  // 使用自定义的文件名,或生成随机文件名
  final baseName = job.exportFileName ?? generateRandomName();
  final fileName = '$baseName.e2e';
  final savePath = '${downloadDir.path}/$fileName';
  // ...
}

5. 导出对话框 - 添加文件名输入框

Future<void> _showExportE2EDialog() async {
  final fileNameController = TextEditingController(text: generateRandomName());
  // ...
  
  content: SingleChildScrollView(
    child: Column(
      children: [
        Text('设置密码用于保护文件 "${widget.file.name}"'),
        TextField(
          controller: fileNameController,
          decoration: const InputDecoration(
            labelText: '文件名',
            border: OutlineInputBorder(),
            suffixText: '.e2e',  // 显示扩展名后缀
          ),
        ),
        // ... 密码输入框 ...
      ],
    ),
  ),
}

10.4 UI 优化:修复对话框溢出

问题:键盘弹出时,对话框内容溢出(BOTTOM OVERFLOWED BY 37 PIXELS)

原因AlertDialogcontent 使用 Column,内容固定高度

解决:用 SingleChildScrollView 包裹 Column,让内容可滚动

// 修复前
content: Column(
  mainAxisSize: MainAxisSize.min,
  children: [...],
),
 
// 修复后
content: SingleChildScrollView(
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [...],
  ),
),

十一、文件图标支持扩展

11.1 需求

支持 APK 文件和代码文件的专属图标,与现有图标保持同一系列(Material Design Icons)。

11.2 实现

新增扩展名定义

/// 代码文件扩展名
const _codeExtensions = {
  'dart', 'py', 'go', 'java', 'kt', 'swift', 'c', 'cpp', 'h', 'hpp',
  'js', 'ts', 'jsx', 'tsx', 'vue', 'rs', 'rb', 'php', 'cs', 'scala',
  'sh', 'bat', 'ps1', 'sql', 'r', 'lua', 'pl', 'groovy', 'gradle',
};

新增判断函数

/// 判断是否为代码文件
bool isCodeFile(String mimeType, String fileName) {
  final ext = fileName.split('.').last.toLowerCase();
  return _codeExtensions.contains(ext);
}
 
/// 判断是否为 APK 文件
bool isApkFile(String mimeType, String fileName) {
  if (mimeType.contains('android.package') || mimeType.contains('vnd.android')) {
    return true;
  }
  final ext = fileName.split('.').last.toLowerCase();
  return ext == 'apk';
}

图标映射

IconData getFileIcon(String mimeType, {bool isDir = false, String? fileName}) {
  // ...
  
  // APK 文件
  if (name.isNotEmpty && isApkFile(mime, name)) {
    return MdiIcons.android;
  }
  
  // 代码文件
  if (name.isNotEmpty && isCodeFile(mime, name)) {
    return MdiIcons.fileCode;
  }
  
  // ... 其他类型 ...
}

颜色映射

Color getFileIconColor(String mimeType, {bool isDir = false, String? fileName}) {
  // ...
  
  // APK 文件 - 浅绿色(Android 主题色)
  if (name.isNotEmpty && isApkFile(mime, name)) {
    return Colors.lightGreen;
  }
  
  // 代码文件 - 青色
  if (name.isNotEmpty && isCodeFile(mime, name)) {
    return Colors.teal;
  }
  
  // ... 其他类型 ...
}

11.3 图标对照表

文件类型图标颜色扩展名示例
文件夹folderamber-
图片fileImagegreenjpg, png, gif
视频fileVideobluemp4, mkv, mov
音频fileMusicpurplemp3, wav, flac
PDFfilePdfBoxredpdf
压缩包folderZipgreyzip, rar, 7z
文本fileDocumentOutlinegreytxt, md, json
APKandroidlightGreenapk
代码fileCodetealdart, py, go, js
其他filegrey-

十二、总结

12.1 本次重构的关键改动

  1. 架构简化:从“独立实现”回归“接口复用”
  2. 代码清理:删除旧版 API 和废弃函数
  3. 功能增强:导出支持自定义文件名
  4. UI 优化:修复对话框溢出问题
  5. 图标扩展:支持 APK 和代码文件

12.2 修改文件清单

文件修改类型说明
server.go删除+新增删除旧 API,保留简化版
api_client.dart删除+新增删除旧方法,保留简化版
app_state.dart重构导入导出逻辑简化
files_page.dart修改导出对话框添加文件名输入
file_utils.dart新增随机名函数、APK/代码图标
e2e_progress.dart重写简化为结果类型

12.3 经验教训

  1. 复用优先:新功能应尽量复用现有基础设施
  2. 简单优于复杂:不要为了“性能优化”而过度设计
  3. 使用直觉:第一反应通常是正确的
  4. 及时清理:重构后立即删除废弃代码

更新时间: 2025-12-22 00:38