143 lines
6.5 KiB
Dart
143 lines
6.5 KiB
Dart
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<TransferJob> jobs;
|
|
|
|
/// Rsync 화면 (작업 줄 모양이 다름)
|
|
final bool rsync;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 85: 숨겼으면 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (누르면 다시 펼침)
|
|
if (jobs.isNotEmpty && c.settings.explorerProgressHidden) {
|
|
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<double>(0, (a, j) => a + j.overall) / run.length * 100).round();
|
|
return TransferProgressChip(
|
|
running: run.length,
|
|
percent: pct,
|
|
failed: failed,
|
|
onTap: () => c.updateSettings((x) => x.explorerProgressHidden = 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.explorerProgressHidden = 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)),
|
|
]),
|
|
),
|
|
),
|
|
);
|
|
}
|