import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../app/transfer_job.dart'; import '../../core/file_ops.dart' show formatSize; import '../../core/vfs.dart' show vBasename; import '../../l10n/tr.dart'; import '../theme.dart'; /// 약속 (E5d-4): 복사 · 이동 작업 하나의 진행 - 제목 (복사 · 옮기는 중 · 완료 · 멈춤 + 방법) · 대상 · [취소] (도는 동안). /// 파일 탐색기: 위 = 전체 항목 중 · 아래 = 지금 폴더의 파일 중. [rsync] (폴더 하나를 맞춤): 위 = 지금 파일 · 아래 = 폴더 전체. /// 그리기만 한다 (작업이 바뀌면 부르는 쪽이 다시 그림) class TransferJobView extends StatelessWidget { const TransferJobView({super.key, required this.job, required this.rsync}); final TransferJob job; final bool rsync; @override Widget build(BuildContext context) { String pct(double v) => '${(v * 100).round()}%'; final cur = job.index < job.total ? job.index : job.total - 1; final curFiles = job.total == 0 ? 0 : job.filesTotal[cur.clamp(0, job.total - 1)]; final curDone = job.total == 0 ? 0 : job.filesDone[cur.clamp(0, job.total - 1)]; final title = job.finished ? (job.error == null ? tr('완료') : tr('멈춤')) : job.move ? tr('옮기는 중') : tr('복사하는 중'); final labelMax = MediaQuery.sizeOf(context).width * 0.45; Widget line(String label, double value, String right) => Padding( padding: const EdgeInsets.only(top: 6), child: Row(children: [ SizedBox( width: math.min(300, labelMax), // 좁은 화면 (폰 세로) 은 폭의 45% 까지 child: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12.5)), ), const SizedBox(width: 10), Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(3), child: LinearProgressIndicator( minHeight: 8, value: job.counting ? null : value, // 빈 곳과 찬 곳이 분명히 다르게 color: JjColors.accent, backgroundColor: JjColors.border, ), ), ), const SizedBox(width: 10), SizedBox( width: 150, child: Text(right, textAlign: TextAlign.end, style: TextStyle(fontSize: 12, color: JjColors.textDim)), ), ]), ); return Column(mainAxisSize: MainAxisSize.min, children: [ Row(children: [ Icon(job.move ? Icons.drive_file_move_outline : Icons.copy_outlined, size: 18, color: JjColors.accent), const SizedBox(width: 8), Text('$title · ${tr(job.method.label)}', style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(width: 12), Expanded( // Rsync 화면: 원본 → 대상 (⇄ 는 두 줄) child: Text(rsync ? '${job.sources.join(', ')}/ → ${job.dest}/' : job.dest, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)), ), if (!job.finished) TextButton(onPressed: job.cancel, child: Text(tr('취소'))), ]), // Rsync 화면 (폴더 하나를 맞춤): 위 = 지금 복사하는 파일 (그 파일의 진행률 · 속도), 아래 = 폴더 전체 (파일 수) if (rsync) ...[ line( job.counting ? tr('파일 세는 중…') : job.pruning ? (job.finished ? trf('빈 폴더 {0}개 지움', [job.pruned]) : tr('원본의 빈 폴더 정리 중…')) : job.finished ? tr('끝') : job.currentFile.isEmpty ? tr('시작하는 중…') : job.currentFile.split('/').last, job.finished ? 1.0 : (job.filePercent ?? 0), [ if (job.filePercent != null && !job.finished) pct(job.filePercent!), if (!job.finished && job.speed != null) '${formatSize(job.speed!.round())}/s', ].join(' · '), ), line( trf('{0} 전체', [job.currentName.isEmpty ? vBasename(job.sources.first) : job.currentName]), job.folderProgress, '${pct(job.folderProgress)} · ${job.checkTotal > 0 && !job.finished ? trf('항목 {0}/{1}', [job.checked, job.checkTotal]) : trf('파일 {0}/{1}', [job.allDone, job.allFiles])}', ), ] else ...[ // 파일 탐색기 (여러 항목): 위 = 전체 항목 중 · 아래 = 지금 폴더의 파일 중 line( trf('전체 {0}개 중 {1}번째', [job.total, (cur + 1).clamp(1, job.total)]), job.overall, '${pct(job.overall)} · ${trf('파일 {0}/{1}', [job.allDone, job.allFiles])}' '${!job.finished && job.speed != null ? ' · ${formatSize(job.speed!.round())}/s' : ''}', ), line( job.counting ? tr('파일 세는 중…') : job.pruning ? (job.finished ? trf('빈 폴더 {0}개 지움', [job.pruned]) : tr('원본의 빈 폴더 정리 중…')) : trf('{0} 안의 파일', [job.currentName]), job.current, '${pct(job.current)} · ${trf('파일 {0}/{1}', [curDone, curFiles])}', ), ], ]); } }