Flutter 框架跨平台鸿蒙开发 - 简易字幕制作器:打造专业级字幕编辑工具

Flutter简易字幕制作器:打造专业级字幕编辑工具

项目概述

简易字幕制作器是一款基于Flutter开发的专业字幕编辑应用,为视频创作者和字幕制作人员提供完整的字幕制作解决方案。应用集成了字幕编辑、时间轴管理、实时预览、多格式导出等核心功能,通过直观的界面设计和强大的编辑能力,让字幕制作变得简单高效。
运行效果图




应用特色

  • 可视化编辑:直观的字幕编辑界面,支持实时预览
  • 时间轴管理:专业的时间轴视图,精确控制字幕时间
  • 多格式导出:支持SRT、VTT、TXT等主流字幕格式
  • 项目管理:完整的项目管理系统,支持多项目切换
  • 播放控制:内置播放器,实时预览字幕效果
  • 智能排列:自动调整字幕时间,避免重叠冲突

技术架构

核心技术栈

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

项目结构

复制代码
lib/
├── main.dart                    # 应用入口和主要逻辑
├── models/                     # 数据模型(集成在main.dart中)
│   ├── subtitle_item.dart       # 字幕条目模型
│   ├── subtitle_project.dart    # 字幕项目模型
│   └── player_state.dart        # 播放器状态枚举
├── screens/                    # 页面组件(集成在main.dart中)
│   ├── editor_page.dart         # 编辑页面
│   ├── timeline_page.dart       # 时间轴页面
│   ├── preview_page.dart        # 预览页面
│   └── projects_page.dart       # 项目页面
├── widgets/                    # 自定义组件(集成在main.dart中)
│   ├── player_controls.dart     # 播放控制器
│   ├── subtitle_editor.dart     # 字幕编辑器
│   ├── timeline_view.dart       # 时间轴视图
│   └── subtitle_list.dart       # 字幕列表
└── utils/                      # 工具类(集成在main.dart中)
    ├── time_formatter.dart      # 时间格式化
    ├── subtitle_exporter.dart   # 字幕导出
    └── timeline_painter.dart    # 时间轴绘制

数据模型设计

字幕条目模型(SubtitleItem)

字幕条目是应用的核心数据结构,包含字幕的所有属性:

dart 复制代码
class SubtitleItem {
  final String id;              // 唯一标识
  String text;                  // 字幕文本
  Duration startTime;           // 开始时间
  Duration endTime;             // 结束时间
  TextStyle style;              // 文本样式
  Alignment alignment;          // 对齐方式
  Color backgroundColor;        // 背景颜色
  double opacity;               // 透明度

  SubtitleItem({
    required this.id,
    required this.text,
    required this.startTime,
    required this.endTime,
    this.style = const TextStyle(
      fontSize: 16,
      color: Colors.white,
      fontWeight: FontWeight.normal,
    ),
    this.alignment = Alignment.bottomCenter,
    this.backgroundColor = Colors.black54,
    this.opacity = 0.8,
  });
}

字幕条目模型包含实用的计算属性和方法:

dart 复制代码
// 计算字幕持续时间
Duration get duration => endTime - startTime;

// 判断字幕在指定时间是否激活
bool isActiveAt(Duration time) {
  return time >= startTime && time <= endTime;
}

// 创建字幕副本(用于编辑)
SubtitleItem copyWith({
  String? text,
  Duration? startTime,
  Duration? endTime,
  TextStyle? style,
  Alignment? alignment,
  Color? backgroundColor,
  double? opacity,
}) {
  return SubtitleItem(
    id: id,
    text: text ?? this.text,
    startTime: startTime ?? this.startTime,
    endTime: endTime ?? this.endTime,
    style: style ?? this.style,
    alignment: alignment ?? this.alignment,
    backgroundColor: backgroundColor ?? this.backgroundColor,
    opacity: opacity ?? this.opacity,
  );
}

字幕项目模型(SubtitleProject)

字幕项目模型管理整个字幕制作项目:

dart 复制代码
class SubtitleProject {
  final String id;                    // 项目唯一标识
  String name;                        // 项目名称
  Duration totalDuration;             // 项目总时长
  List<SubtitleItem> subtitles;       // 字幕列表
  DateTime createdAt;                 // 创建时间
  DateTime updatedAt;                 // 更新时间

  SubtitleProject({
    required this.id,
    required this.name,
    required this.totalDuration,
    required this.subtitles,
    required this.createdAt,
    required this.updatedAt,
  });
}

项目模型提供完整的字幕管理功能:

dart 复制代码
// 添加字幕(自动排序)
void addSubtitle(SubtitleItem subtitle) {
  subtitles.add(subtitle);
  subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
  updatedAt = DateTime.now();
}

// 删除字幕
void removeSubtitle(String id) {
  subtitles.removeWhere((subtitle) => subtitle.id == id);
  updatedAt = DateTime.now();
}

// 更新字幕
void updateSubtitle(String id, SubtitleItem newSubtitle) {
  final index = subtitles.indexWhere((subtitle) => subtitle.id == id);
  if (index != -1) {
    subtitles[index] = newSubtitle;
    subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));
    updatedAt = DateTime.now();
  }
}

// 获取指定时间的激活字幕
List<SubtitleItem> getActiveSubtitles(Duration time) {
  return subtitles.where((subtitle) => subtitle.isActiveAt(time)).toList();
}

播放器状态枚举

播放器状态管理播放控制:

dart 复制代码
enum PlayerState { 
  stopped,    // 停止
  playing,    // 播放中
  paused      // 暂停
}

应用主体结构

应用入口

dart 复制代码
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '简易字幕制作器',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SubtitleEditorHomePage(),
    );
  }
}

应用采用深紫色作为主题色,营造专业而现代的视觉效果。

主页面结构

主页面使用底部导航栏实现四个核心功能模块:

dart 复制代码
class SubtitleEditorHomePage extends StatefulWidget {
  const SubtitleEditorHomePage({super.key});

  @override
  State<SubtitleEditorHomePage> createState() => _SubtitleEditorHomePageState();
}

class _SubtitleEditorHomePageState extends State<SubtitleEditorHomePage>
    with TickerProviderStateMixin {
  int _selectedIndex = 0;
  List<SubtitleProject> _projects = [];      // 项目列表
  SubtitleProject? _currentProject;          // 当前项目
  
  // 播放器相关
  PlayerState _playerState = PlayerState.stopped;
  Duration _currentTime = Duration.zero;
  Duration _totalDuration = const Duration(minutes: 5);
  Timer? _playTimer;
  
  // 编辑相关
  SubtitleItem? _selectedSubtitle;
  final TextEditingController _textController = TextEditingController();
  final TextEditingController _startTimeController = TextEditingController();
  final TextEditingController _endTimeController = TextEditingController();
  
  // 动画控制器
  late AnimationController _timelineAnimationController;
  late Animation<double> _timelineAnimation;
}

编辑功能实现

播放控制器

播放控制器是字幕编辑的核心组件:

dart 复制代码
Widget _buildPlayerControls() {
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.grey.shade100,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.grey.shade300),
    ),
    child: Column(
      children: [
        // 时间显示
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              _formatDuration(_currentTime),
              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            Text(
              _formatDuration(_totalDuration),
              style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
            ),
          ],
        ),
        
        const SizedBox(height: 8),
        
        // 进度条
        SliderTheme(
          data: SliderTheme.of(context).copyWith(
            trackHeight: 4,
            thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
          ),
          child: Slider(
            value: _currentTime.inMilliseconds.toDouble(),
            max: _totalDuration.inMilliseconds.toDouble(),
            onChanged: (value) {
              setState(() {
                _currentTime = Duration(milliseconds: value.toInt());
              });
            },
            activeColor: Colors.deepPurple,
          ),
        ),
        
        const SizedBox(height: 12),
        
        // 播放控制按钮
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              onPressed: _skipBackward,
              icon: const Icon(Icons.skip_previous),
              iconSize: 32,
            ),
            const SizedBox(width: 16),
            Container(
              decoration: BoxDecoration(
                color: Colors.deepPurple,
                shape: BoxShape.circle,
              ),
              child: IconButton(
                onPressed: _togglePlayPause,
                icon: Icon(
                  _playerState == PlayerState.playing
                      ? Icons.pause
                      : Icons.play_arrow,
                  color: Colors.white,
                ),
                iconSize: 32,
              ),
            ),
            const SizedBox(width: 16),
            IconButton(
              onPressed: _skipForward,
              icon: const Icon(Icons.skip_next),
              iconSize: 32,
            ),
          ],
        ),
      ],
    ),
  );
}

播放控制逻辑

播放控制通过定时器实现时间更新:

dart 复制代码
void _togglePlayPause() {
  setState(() {
    if (_playerState == PlayerState.playing) {
      _playerState = PlayerState.paused;
      _playTimer?.cancel();
    } else {
      _playerState = PlayerState.playing;
      _startPlayback();
    }
  });
}

void _startPlayback() {
  _playTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
    setState(() {
      _currentTime += const Duration(milliseconds: 100);
      if (_currentTime >= _totalDuration) {
        _currentTime = _totalDuration;
        _playerState = PlayerState.stopped;
        timer.cancel();
      }
    });
  });
}

void _skipBackward() {
  setState(() {
    _currentTime = Duration(
      milliseconds: (_currentTime.inMilliseconds - 5000).clamp(0, _totalDuration.inMilliseconds),
    );
  });
}

void _skipForward() {
  setState(() {
    _currentTime = Duration(
      milliseconds: (_currentTime.inMilliseconds + 5000).clamp(0, _totalDuration.inMilliseconds),
    );
  });
}

字幕编辑器

字幕编辑器提供完整的字幕编辑功能:

dart 复制代码
Widget _buildSubtitleEditor() {
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.grey.shade300),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 编辑器标题和操作按钮
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            const Text(
              '字幕编辑',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            Row(
              children: [
                ElevatedButton.icon(
                  onPressed: _addNewSubtitle,
                  icon: const Icon(Icons.add, size: 16),
                  label: const Text('新增'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                    foregroundColor: Colors.white,
                    minimumSize: const Size(80, 32),
                  ),
                ),
                const SizedBox(width: 8),
                if (_selectedSubtitle != null)
                  ElevatedButton.icon(
                    onPressed: _deleteSelectedSubtitle,
                    icon: const Icon(Icons.delete, size: 16),
                    label: const Text('删除'),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.red,
                      foregroundColor: Colors.white,
                      minimumSize: const Size(80, 32),
                    ),
                  ),
              ],
            ),
          ],
        ),
        
        const SizedBox(height: 16),
        
        // 字幕文本编辑
        TextField(
          controller: _textController,
          decoration: const InputDecoration(
            labelText: '字幕内容',
            hintText: '请输入字幕文本...',
            border: OutlineInputBorder(),
          ),
          maxLines: 3,
          onChanged: _updateSelectedSubtitleText,
        ),
        
        const SizedBox(height: 16),
        
        // 时间设置
        Row(
          children: [
            Expanded(
              child: TextField(
                controller: _startTimeController,
                decoration: const InputDecoration(
                  labelText: '开始时间',
                  hintText: '00:00:00',
                  border: OutlineInputBorder(),
                ),
                onChanged: _updateStartTime,
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: TextField(
                controller: _endTimeController,
                decoration: const InputDecoration(
                  labelText: '结束时间',
                  hintText: '00:00:03',
                  border: OutlineInputBorder(),
                ),
                onChanged: _updateEndTime,
              ),
            ),
          ],
        ),
        
        const SizedBox(height: 16),
        
        // 快速时间设置按钮
        Wrap(
          spacing: 8,
          children: [
            _buildQuickTimeButton('当前时间', () => _setCurrentTime(true)),
            _buildQuickTimeButton('+3秒', () => _adjustTime(3)),
            _buildQuickTimeButton('+5秒', () => _adjustTime(5)),
            _buildQuickTimeButton('-1秒', () => _adjustTime(-1)),
          ],
        ),
        
        const SizedBox(height: 16),
        
        // 保存按钮
        if (_selectedSubtitle != null)
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: _saveSubtitleChanges,
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.deepPurple,
                foregroundColor: Colors.white,
              ),
              child: const Text('保存修改'),
            ),
          ),
      ],
    ),
  );
}

字幕编辑逻辑

字幕编辑的核心逻辑实现:

dart 复制代码
void _addNewSubtitle() {
  if (_currentProject == null) return;

  final newSubtitle = SubtitleItem(
    id: 'sub_${DateTime.now().millisecondsSinceEpoch}',
    text: '新字幕',
    startTime: _currentTime,
    endTime: _currentTime + const Duration(seconds: 3),
  );

  setState(() {
    _currentProject!.addSubtitle(newSubtitle);
    _selectedSubtitle = newSubtitle;
    _updateEditorFields();
  });
}

void _selectSubtitle(SubtitleItem subtitle) {
  setState(() {
    _selectedSubtitle = subtitle;
    _updateEditorFields();
  });
}

void _updateEditorFields() {
  if (_selectedSubtitle != null) {
    _textController.text = _selectedSubtitle!.text;
    _startTimeController.text = _formatDuration(_selectedSubtitle!.startTime);
    _endTimeController.text = _formatDuration(_selectedSubtitle!.endTime);
  }
}

void _saveSubtitleChanges() {
  if (_selectedSubtitle == null || _currentProject == null) return;

  setState(() {
    _currentProject!.updateSubtitle(_selectedSubtitle!.id, _selectedSubtitle!);
  });

  _showMessage('字幕已保存');
}

时间轴功能

时间轴视图

时间轴提供可视化的字幕时间管理:

dart 复制代码
Widget _buildTimeline() {
  return Container(
    height: 300,
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.grey.shade300),
    ),
    child: Column(
      children: [
        // 时间刻度
        Container(
          height: 40,
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            children: List.generate(11, (index) {
              final time = Duration(
                milliseconds: (_totalDuration.inMilliseconds * index / 10).round(),
              );
              return Expanded(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Container(
                      width: 1,
                      height: 20,
                      color: Colors.grey.shade400,
                    ),
                    const SizedBox(height: 4),
                    Text(
                      _formatDuration(time),
                      style: const TextStyle(fontSize: 10),
                    ),
                  ],
                ),
              );
            }),
          ),
        ),

        // 字幕轨道
        Expanded(
          child: Stack(
            children: [
              // 背景网格
              CustomPaint(
                size: const Size(double.infinity, double.infinity),
                painter: TimelineGridPainter(),
              ),

              // 字幕条
              ...(_currentProject?.subtitles.map((subtitle) {
                return _buildSubtitleBar(subtitle);
              }) ?? []),

              // 播放指针
              Positioned(
                left: (_currentTime.inMilliseconds / _totalDuration.inMilliseconds) * 
                      (MediaQuery.of(context).size.width - 32) + 16,
                top: 0,
                bottom: 0,
                child: Container(
                  width: 2,
                  color: Colors.red,
                  child: const Align(
                    alignment: Alignment.topCenter,
                    child: Icon(
                      Icons.play_arrow,
                      color: Colors.red,
                      size: 16,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

字幕条组件

时间轴上的字幕条显示:

dart 复制代码
Widget _buildSubtitleBar(SubtitleItem subtitle) {
  final screenWidth = MediaQuery.of(context).size.width - 32;
  final startPosition = (subtitle.startTime.inMilliseconds / _totalDuration.inMilliseconds) * screenWidth;
  final width = (subtitle.duration.inMilliseconds / _totalDuration.inMilliseconds) * screenWidth;
  final isSelected = _selectedSubtitle?.id == subtitle.id;

  return Positioned(
    left: startPosition + 16,
    top: 60,
    width: width.clamp(20.0, screenWidth),
    height: 40,
    child: GestureDetector(
      onTap: () => _selectSubtitle(subtitle),
      child: Container(
        decoration: BoxDecoration(
          color: isSelected ? Colors.deepPurple : Colors.blue.shade300,
          borderRadius: BorderRadius.circular(4),
          border: Border.all(
            color: isSelected ? Colors.deepPurple.shade700 : Colors.blue.shade500,
            width: 1,
          ),
        ),
        padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              subtitle.text,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 10,
                fontWeight: FontWeight.bold,
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            Text(
              '${_formatDuration(subtitle.startTime)} - ${_formatDuration(subtitle.endTime)}',
              style: const TextStyle(
                color: Colors.white70,
                fontSize: 8,
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

时间轴网格绘制

自定义绘制器实现时间轴网格:

dart 复制代码
class TimelineGridPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.grey.shade300
      ..strokeWidth = 0.5;

    // 绘制垂直网格线
    for (int i = 0; i <= 10; i++) {
      final x = (size.width * i / 10);
      canvas.drawLine(
        Offset(x, 0),
        Offset(x, size.height),
        paint,
      );
    }

    // 绘制水平网格线
    for (int i = 0; i <= 5; i++) {
      final y = (size.height * i / 5);
      canvas.drawLine(
        Offset(0, y),
        Offset(size.width, y),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

预览功能

预览页面设计

预览页面提供实时的字幕效果预览:

dart 复制代码
Widget _buildPreviewPage() {
  if (_currentProject == null) {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.play_circle_outline, size: 64, color: Colors.grey),
          SizedBox(height: 16),
          Text(
            '请先创建或选择一个项目',
            style: TextStyle(fontSize: 18, color: Colors.grey),
          ),
        ],
      ),
    );
  }

  final activeSubtitles = _currentProject!.getActiveSubtitles(_currentTime);

  return Column(
    children: [
      // 预览区域
      Expanded(
        child: Container(
          width: double.infinity,
          margin: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: Colors.black,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Stack(
            children: [
              // 背景
              const Center(
                child: Icon(
                  Icons.movie,
                  size: 64,
                  color: Colors.white24,
                ),
              ),

              // 字幕显示
              ...activeSubtitles.map((subtitle) {
                return Positioned.fill(
                  child: Align(
                    alignment: subtitle.alignment,
                    child: Container(
                      margin: const EdgeInsets.all(16),
                      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                      decoration: BoxDecoration(
                        color: subtitle.backgroundColor.withValues(alpha: subtitle.opacity),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Text(
                        subtitle.text,
                        style: subtitle.style,
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ),
                );
              }),

              // 时间显示
              Positioned(
                top: 16,
                left: 16,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: Colors.black54,
                    borderRadius: BorderRadius.circular(4),
                  ),
                  child: Text(
                    _formatDuration(_currentTime),
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),

      // 预览控制
      Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 播放控制
            _buildPlayerControls(),

            const SizedBox(height: 16),

            // 预览选项
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton.icon(
                  onPressed: _toggleFullscreen,
                  icon: const Icon(Icons.fullscreen),
                  label: const Text('全屏'),
                ),
                ElevatedButton.icon(
                  onPressed: _showSubtitleSettings,
                  icon: const Icon(Icons.text_fields),
                  label: const Text('样式'),
                ),
                ElevatedButton.icon(
                  onPressed: _exportVideo,
                  icon: const Icon(Icons.video_file),
                  label: const Text('导出'),
                ),
              ],
            ),
          ],
        ),
      ),
    ],
  );
}

项目管理

项目列表页面

项目管理页面提供完整的项目管理功能:

dart 复制代码
Widget _buildProjectsPage() {
  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 项目管理头部
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            const Text(
              '我的项目',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            ElevatedButton.icon(
              onPressed: _createNewProject,
              icon: const Icon(Icons.add),
              label: const Text('新建项目'),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.deepPurple,
                foregroundColor: Colors.white,
              ),
            ),
          ],
        ),

        const SizedBox(height: 20),

        // 项目统计
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.deepPurple.shade400, Colors.blue.shade400],
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
            ),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Row(
            children: [
              _buildProjectStatCard('项目总数', '${_projects.length}'),
              const SizedBox(width: 16),
              _buildProjectStatCard('字幕总数', '${_getTotalSubtitles()}'),
            ],
          ),
        ),

        const SizedBox(height: 24),

        // 项目列表
        ListView.builder(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          itemCount: _projects.length,
          itemBuilder: (context, index) {
            final project = _projects[index];
            final isActive = _currentProject?.id == project.id;

            return Container(
              margin: const EdgeInsets.only(bottom: 12),
              child: Card(
                elevation: isActive ? 4 : 1,
                color: isActive ? Colors.deepPurple.shade50 : null,
                child: ListTile(
                  leading: Container(
                    width: 48,
                    height: 48,
                    decoration: BoxDecoration(
                      color: isActive ? Colors.deepPurple : Colors.grey.shade400,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Icon(
                      Icons.movie_creation,
                      color: Colors.white,
                      size: 24,
                    ),
                  ),
                  title: Text(
                    project.name,
                    style: TextStyle(
                      fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
                    ),
                  ),
                  subtitle: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('${project.subtitles.length}条字幕 • ${_formatDuration(project.totalDuration)}'),
                      Text(
                        '更新于 ${_formatDateTime(project.updatedAt)}',
                        style: const TextStyle(fontSize: 12),
                      ),
                    ],
                  ),
                  trailing: PopupMenuButton<String>(
                    onSelected: (value) => _handleProjectAction(value, project),
                    itemBuilder: (context) => [
                      const PopupMenuItem(
                        value: 'open',
                        child: Row(
                          children: [
                            Icon(Icons.open_in_new, size: 16),
                            SizedBox(width: 8),
                            Text('打开'),
                          ],
                        ),
                      ),
                      const PopupMenuItem(
                        value: 'rename',
                        child: Row(
                          children: [
                            Icon(Icons.edit, size: 16),
                            SizedBox(width: 8),
                            Text('重命名'),
                          ],
                        ),
                      ),
                      const PopupMenuItem(
                        value: 'duplicate',
                        child: Row(
                          children: [
                            Icon(Icons.copy, size: 16),
                            SizedBox(width: 8),
                            Text('复制'),
                          ],
                        ),
                      ),
                      const PopupMenuItem(
                        value: 'delete',
                        child: Row(
                          children: [
                            Icon(Icons.delete, size: 16, color: Colors.red),
                            SizedBox(width: 8),
                            Text('删除', style: TextStyle(color: Colors.red)),
                          ],
                        ),
                      ),
                    ],
                  ),
                  onTap: () => _openProject(project),
                ),
              ),
            );
          },
        ),
      ],
    ),
  );
}

项目创建功能

新建项目的对话框实现:

dart 复制代码
void _createNewProject() {
  showDialog(
    context: context,
    builder: (context) {
      final nameController = TextEditingController();
      final durationController = TextEditingController(text: '5:00');

      return AlertDialog(
        title: const Text('新建项目'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: nameController,
              decoration: const InputDecoration(
                labelText: '项目名称',
                hintText: '请输入项目名称',
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: durationController,
              decoration: const InputDecoration(
                labelText: '项目时长',
                hintText: '格式: 分:秒 (如 5:00)',
              ),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              final name = nameController.text.trim();
              final durationStr = durationController.text.trim();
              
              if (name.isEmpty) {
                _showMessage('请输入项目名称');
                return;
              }

              final duration = _parseDuration('00:$durationStr') ?? const Duration(minutes: 5);
              
              final newProject = SubtitleProject(
                id: 'project_${DateTime.now().millisecondsSinceEpoch}',
                name: name,
                totalDuration: duration,
                subtitles: [],
                createdAt: DateTime.now(),
                updatedAt: DateTime.now(),
              );

              setState(() {
                _projects.add(newProject);
                _currentProject = newProject;
                _totalDuration = duration;
                _currentTime = Duration.zero;
                _selectedSubtitle = null;
                _clearEditorFields();
              });

              Navigator.of(context).pop();
              _showMessage('项目创建成功');
            },
            child: const Text('创建'),
          ),
        ],
      );
    },
  );
}

导出功能

多格式导出

应用支持多种字幕格式的导出:

dart 复制代码
void _exportSubtitles() {
  if (_currentProject == null || _currentProject!.subtitles.isEmpty) {
    _showMessage('没有字幕可以导出');
    return;
  }

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('导出字幕'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.text_snippet),
            title: const Text('SRT格式'),
            subtitle: const Text('通用字幕格式'),
            onTap: () {
              Navigator.of(context).pop();
              _exportToSRT();
            },
          ),
          ListTile(
            leading: const Icon(Icons.code),
            title: const Text('VTT格式'),
            subtitle: const Text('Web视频字幕格式'),
            onTap: () {
              Navigator.of(context).pop();
              _exportToVTT();
            },
          ),
          ListTile(
            leading: const Icon(Icons.description),
            title: const Text('TXT格式'),
            subtitle: const Text('纯文本格式'),
            onTap: () {
              Navigator.of(context).pop();
              _exportToTXT();
            },
          ),
        ],
      ),
    ),
  );
}

SRT格式导出

SRT是最常用的字幕格式:

dart 复制代码
String _generateSRTContent() {
  final buffer = StringBuffer();
  final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
  subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));

  for (int i = 0; i < subtitles.length; i++) {
    final subtitle = subtitles[i];
    buffer.writeln('${i + 1}');
    buffer.writeln('${_formatSRTTime(subtitle.startTime)} --> ${_formatSRTTime(subtitle.endTime)}');
    buffer.writeln(subtitle.text);
    buffer.writeln();
  }

  return buffer.toString();
}

String _formatSRTTime(Duration duration) {
  final hours = duration.inHours;
  final minutes = duration.inMinutes % 60;
  final seconds = duration.inSeconds % 60;
  final milliseconds = duration.inMilliseconds % 1000;
  return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')},${milliseconds.toString().padLeft(3, '0')}';
}

VTT格式导出

VTT格式适用于Web视频:

dart 复制代码
String _generateVTTContent() {
  final buffer = StringBuffer();
  buffer.writeln('WEBVTT');
  buffer.writeln();

  final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
  subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));

  for (final subtitle in subtitles) {
    buffer.writeln('${_formatVTTTime(subtitle.startTime)} --> ${_formatVTTTime(subtitle.endTime)}');
    buffer.writeln(subtitle.text);
    buffer.writeln();
  }

  return buffer.toString();
}

String _formatVTTTime(Duration duration) {
  final hours = duration.inHours;
  final minutes = duration.inMinutes % 60;
  final seconds = duration.inSeconds % 60;
  final milliseconds = duration.inMilliseconds % 1000;
  return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}.${milliseconds.toString().padLeft(3, '0')}';
}

TXT格式导出

TXT格式提供简单的文本输出:

dart 复制代码
String _generateTXTContent() {
  final buffer = StringBuffer();
  final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
  subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));

  for (final subtitle in subtitles) {
    buffer.writeln('[${_formatDuration(subtitle.startTime)} - ${_formatDuration(subtitle.endTime)}]');
    buffer.writeln(subtitle.text);
    buffer.writeln();
  }

  return buffer.toString();
}

智能功能

自动排列字幕

自动调整字幕时间,避免重叠:

dart 复制代码
void _autoArrangeSubtitles() {
  if (_currentProject == null || _currentProject!.subtitles.isEmpty) return;

  // 自动调整字幕时间,避免重叠
  final subtitles = List<SubtitleItem>.from(_currentProject!.subtitles);
  subtitles.sort((a, b) => a.startTime.compareTo(b.startTime));

  for (int i = 1; i < subtitles.length; i++) {
    final current = subtitles[i];
    final previous = subtitles[i - 1];

    if (current.startTime < previous.endTime) {
      // 调整当前字幕的开始时间
      final newStartTime = previous.endTime + const Duration(milliseconds: 500);
      final duration = current.duration;
      
      subtitles[i] = current.copyWith(
        startTime: newStartTime,
        endTime: newStartTime + duration,
      );
    }
  }

  setState(() {
    _currentProject!.subtitles = subtitles;
    _currentProject!.updatedAt = DateTime.now();
  });

  _showMessage('字幕时间已自动调整');
}

工具函数

时间格式化

应用包含多种时间格式化函数:

dart 复制代码
// 基本时间格式化(MM:SS)
String _formatDuration(Duration duration) {
  final minutes = duration.inMinutes;
  final seconds = duration.inSeconds % 60;
  return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}

// 日期时间格式化
String _formatDateTime(DateTime dateTime) {
  return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}

// 时间字符串解析
Duration? _parseDuration(String timeStr) {
  try {
    final parts = timeStr.split(':');
    if (parts.length == 2) {
      final minutes = int.parse(parts[0]);
      final seconds = int.parse(parts[1]);
      return Duration(minutes: minutes, seconds: seconds);
    } else if (parts.length == 3) {
      final hours = int.parse(parts[0]);
      final minutes = int.parse(parts[1]);
      final seconds = int.parse(parts[2]);
      return Duration(hours: hours, minutes: minutes, seconds: seconds);
    }
  } catch (e) {
    // 解析失败
  }
  return null;
}

项目总结

简易字幕制作器应用成功实现了完整的字幕制作工作流程,通过专业的界面设计和强大的编辑功能,为用户提供了高效的字幕制作工具。

技术亮点

  1. 可视化时间轴:直观的时间轴管理,支持拖拽和精确编辑
  2. 实时预览:即时预览字幕效果,所见即所得
  3. 多格式导出:支持SRT、VTT、TXT等主流格式
  4. 项目管理:完整的项目管理系统,支持多项目并行
  5. 智能排列:自动调整字幕时间,避免冲突

功能特色

  • 专业的字幕编辑界面和工具
  • 可视化时间轴管理系统
  • 实时播放预览功能
  • 多种字幕格式导出支持
  • 完整的项目管理和版本控制
  • 智能的字幕时间调整算法

扩展方向

  1. 视频集成:支持视频文件导入和同步播放
  2. 样式编辑:丰富的字幕样式和特效设置
  3. 语音识别:自动语音转字幕功能
  4. 协作功能:多人协作编辑和版本管理
  5. 云端同步:项目云端存储和跨设备同步
  6. 批量处理:批量字幕处理和格式转换

通过本教程的学习,你已经掌握了Flutter应用开发的高级技能,包括复杂UI设计、自定义绘制、文件处理和项目管理。这些技能可以应用到媒体处理、创作工具等专业应用的开发中。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
时光慢煮2 小时前
Flutter × OpenHarmony 文件管家-构建文件管理器主界面与存储设备卡片
flutter·华为·开源·openharmony
ITUnicorn2 小时前
【HarmomyOS6】ArkTS入门(三)
华为·harmonyos·arkts·鸿蒙·harmonyos6
[H*]2 小时前
Flutter框架跨平台鸿蒙开发——ListView Widget基础用法
flutter·华为·harmonyos
A懿轩A2 小时前
【2026 最新】Kuikly 编译开发 OpenHarmony 项目逐步详细教程带图操作Android Studio编译(Windows)
windows·harmonyos·鸿蒙·openharmony·kuikly
[H*]2 小时前
Flutter框架跨平台鸿蒙开发——Button样式定制
flutter·华为·harmonyos
不会写代码0002 小时前
Flutter 框架跨平台鸿蒙开发 - 全国图书馆查询:探索知识的殿堂
flutter·华为·harmonyos
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——每日谚语APP的开发流程
flutter·华为·harmonyos·鸿蒙
Whisper_Sy2 小时前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 月报告实现
android·开发语言·javascript·网络·flutter·ecmascript
雨季6662 小时前
Flutter for OpenHarmony 入门实践:从 Scaffold 到 Container 的三段式布局构建
开发语言·javascript·flutter