import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; import '../../app/transfer_job.dart'; import '../../core/file_ops.dart' show FileOpCancelled; import '../../l10n/tr.dart'; import '../theme.dart'; import 'job_view.dart'; /// 약속 (E5d-4): 아래에서 올라오는 복사 · 이동 진행 패널 (작업마다 [TransferJobView]). 작업이 없으면 내려가 숨는다. /// 설정 explorerProgressHidden 이면 오른쪽 아래 작은 표시 ([TransferProgressChip], 멈춘 것이 있으면 빨갛게) - 누르면 다시 펼침 class ExplorerTransferPanel extends StatelessWidget { const ExplorerTransferPanel({super.key, required this.c, required this.jobs, required this.rsync}); final AppController c; final List jobs; /// Rsync 화면 (작업 줄 모양이 다름) final bool rsync; @override Widget build(BuildContext context) { // 85: 숨겼으면 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (누르면 다시 펼침) if (jobs.isNotEmpty && c.settings.explorer.progressHidden) { return Align( alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.only(bottom: 8), child: ListenableBuilder( listenable: Listenable.merge(jobs), builder: (context, _) { final run = [for (final j in jobs) if (!j.finished) j]; // 관리자 판단: 숨겨도 멈춘 (실패한) 것은 분명히 - 빨간 표시 (끝난 알림 · 묻는 창은 숨김과 상관없이 늘 뜬다) final failed = jobs.any((j) => j.finished && j.error != null && j.error is! FileOpCancelled); final pct = run.isEmpty || run.any((j) => j.counting) ? null : (run.fold(0, (a, j) => a + j.overall) / run.length * 100).round(); return TransferProgressChip( running: run.length, percent: pct, failed: failed, onTap: () => c.updateSettings((x) => x.explorer.progressHidden = false)); }, ), ), ); } return AnimatedSlide( offset: jobs.isEmpty ? const Offset(0, 1.2) : Offset.zero, duration: const Duration(milliseconds: 250), curve: Curves.easeOut, child: jobs.isEmpty ? const SizedBox(height: 0) : Material( elevation: 12, color: JjColors.panel, borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Padding( padding: const EdgeInsets.fromLTRB(16, 10, 12, 12), // 여러 개가 함께 돌면 (23) 높이를 넘지 않게 넘겨 보기 child: ConstrainedBox( constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height * 0.45), child: SingleChildScrollView( child: Column(mainAxisSize: MainAxisSize.min, children: [ // 85: 숨기기 (아래 작은 표시로) - 다른 파일을 보면서 기다릴 수 있게. Android 는 알림줄에도 진행률 Align( alignment: Alignment.centerRight, child: SizedBox( height: 28, child: TextButton.icon( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), onPressed: () => c.updateSettings((x) => x.explorer.progressHidden = true), icon: const Icon(Icons.expand_more, size: 18), label: Text(tr('숨기기'), style: const TextStyle(fontSize: 12)), ), ), ), for (final (k, job) in jobs.indexed) ...[ if (k > 0) const Divider(height: 18), ListenableBuilder(listenable: job, builder: (context, _) => TransferJobView(job: job, rsync: rsync)), ], ]), ), ), ), ), ); } } /// 85: 진행 패널을 숨겼을 때 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (멈추면 빨갛게). 누르면 다시 펼침 class TransferProgressChip extends StatelessWidget { /// 도는 것 수 (0 이면 끝) · 진행률 (모르면 null) · 멈춘 (실패한) 것이 있는지 final int running; final int? percent; final bool failed; final VoidCallback onTap; const TransferProgressChip({super.key, required this.running, this.percent, this.failed = false, required this.onTap}); @override Widget build(BuildContext context) => Material( elevation: 6, color: failed ? JjColors.danger.withValues(alpha: 0.25) : JjColors.panelHigh, borderRadius: BorderRadius.circular(18), child: InkWell( borderRadius: BorderRadius.circular(18), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row(mainAxisSize: MainAxisSize.min, children: [ // 멈춤은 진행 고리 대신 경고 표시 (꽉 찬 고리는 "끝났다" 로 보였다) if (failed) Icon(Icons.error_outline, size: 16, color: JjColors.danger) else SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, value: running == 0 ? 1 : (percent == null ? null : percent! / 100)), ), const SizedBox(width: 8), // 좁은 화면 · 큰 글자면 두 줄로 (412 · 글자 1.6배에서 화면을 넘었다 - 253 시험) Flexible( child: Text( failed ? tr('복사 · 이동 멈춤 - 눌러서 보기') : running == 0 ? tr('복사 · 이동 끝') : percent == null ? trf('복사 · 이동 {0}개', [running]) : trf('복사 · 이동 {0}개 · {1}%', [running, percent]), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12.5), ), ), const SizedBox(width: 4), Tooltip(message: tr('진행 패널 보기'), child: const Icon(Icons.expand_less, size: 18)), ]), ), ), ); }