From 3b6d50f40902b3a2154b1a088724864988139994 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:19:46 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20=EC=8B=9C=ED=97=98:=20=EB=AC=B6=EC=9D=8C6?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=BA=A1=EC=B2=98=20(Windows=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=EC=8B=9C=ED=97=98,=20412=20=C2=B7=20960?= =?UTF-8?q?=20=C3=97=20ko=20=C2=B7=20en=20=C2=B7=20ja)=20+=2085=20?= =?UTF-8?q?=EC=9E=91=EC=9D=80=20=ED=91=9C=EC=8B=9C=EB=A5=BC=20TransferProg?= =?UTF-8?q?ressChip=20=EC=9C=BC=EB=A1=9C=20(=EA=B2=80=ED=86=A0=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관리자 지시 5: 기기 없이 15 가 볼 수 있게 integration_test/windows_b6_shot_test.dart (JJ_SHOT_DIR 에 PNG): 78 이름 일괄 창 (겹침 빨간 줄) · 79 찾기 결과 (고른 줄 · 아래 할 일 줄) · 81 빠른 보기 (자막 글) · 82 놓기 묻기 창 · 85 작은 표시 (보통 · 멈춤) · 86 풀기 확인 창 (밖으로 나가는 경로 · 링크 · 쓸 수 없는 이름 · 덮어쓰기 경고) · 87 분석 (큰 폴더 · 큰 파일). 시험 파일은 임시 폴더에만 (끝나면 지움), 설정은 메모리에만. - 85 의 작은 표시를 공개 위젯 TransferProgressChip 으로 뺐다 (모양 · 동작 같음) - 멈춤 상태를 실제 실패 없이 캡처하려고. Co-Authored-By: Claude Opus 5.5 --- integration_test/windows_b6_shot_test.dart | 180 +++++++++++++++++++++ lib/ui/explorer_page.dart | 81 ++++++---- 2 files changed, 229 insertions(+), 32 deletions(-) create mode 100644 integration_test/windows_b6_shot_test.dart diff --git a/integration_test/windows_b6_shot_test.dart b/integration_test/windows_b6_shot_test.dart new file mode 100644 index 0000000..efb10fa --- /dev/null +++ b/integration_test/windows_b6_shot_test.dart @@ -0,0 +1,180 @@ +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:archive/archive_io.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:jj_mkvmaker/app/app_controller.dart'; +import 'package:jj_mkvmaker/app/i18n_controller.dart'; +import 'package:jj_mkvmaker/core/file_ops.dart'; +import 'package:jj_mkvmaker/core/models.dart'; +import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; +import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; +import 'package:jj_mkvmaker/services/platform_services.dart'; +import 'package:jj_mkvmaker/ui/archive_ui.dart'; +import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart'; +import 'package:jj_mkvmaker/ui/explorer_drag.dart'; +import 'package:jj_mkvmaker/ui/explorer_page.dart' show TransferProgressChip; +import 'package:jj_mkvmaker/ui/explorer_search.dart'; +import 'package:jj_mkvmaker/ui/quick_look.dart'; +import 'package:jj_mkvmaker/ui/storage_page.dart'; +import 'package:jj_mkvmaker/ui/theme.dart'; +import 'package:path/path.dart' as p; + +/// 관리자 지시 5 (10/17): 기기 없이 15 가 볼 수 있게 묶음6 새 화면을 PNG 로 (JJ_SHOT_DIR, 없으면 임시 폴더). +/// 78 이름 일괄 창 · 79 찾기 결과 · 81 빠른 보기 · 82 놓기 묻기 창 · 85 작은 표시 (보통 · 멈춤) · 86 풀기 확인 창 (경고 줄) · 87 분석 화면. +/// 크기 412 (폰 세로) · 960, 언어 ko · en · ja. 시험 파일은 임시 폴더에만 (끝나면 지움), 설정은 메모리에만. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_b6'); + + for (final lang in ['ko', 'en', 'ja']) { + for (final (w, h) in [(412, 900), (960, 600)]) { + testWidgets('묶음6 화면 $lang ${w}x$h', (t) async { + t.view.physicalSize = Size(w.toDouble(), h.toDouble()); + t.view.devicePixelRatio = 1; + addTearDown(t.view.reset); + await t.runAsync(() => i18n.apply(lang, save: false)); + addTearDown(() => i18n.apply('ko', save: false)); + final tmp = Directory.systemTemp.createTempSync('jj_b6shot_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + // 시험 파일 + File(p.join(tmp.path, 'Show', 'ep01.mkv')) + ..createSync(recursive: true) + ..writeAsBytesSync(List.filled(4000, 0)); + File(p.join(tmp.path, 'Show', 'ep02.mkv')).writeAsBytesSync(List.filled(3000, 0)); + File(p.join(tmp.path, 'Show', 'Show 01.mkv')).writeAsBytesSync(List.filled(10, 0)); + File(p.join(tmp.path, 'Show', 'ep01.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕 hello\n'); + File(p.join(tmp.path, 'docs', 'note.txt')) + ..createSync(recursive: true) + ..writeAsStringSync('메모 note'); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final navKey = GlobalKey(); + final shotKey = GlobalKey(); + await t.pumpWidget(RepaintBoundary( + key: shotKey, + child: MaterialApp(theme: buildTheme(), navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())), + )); + BuildContext ctx() => navKey.currentContext!; + + Future settle([int n = 8]) async { + for (var i = 0; i < n; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 60))); + await t.pump(); + } + } + + Future shot(String name) async { + await settle(3); + final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final img = await t.runAsync(() => b.toImage()); + final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); + Directory(out).createSync(recursive: true); + File(p.join(out, 'b6_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + } + + Future closeAll() async { + navKey.currentState!.popUntil((r) => r.isFirst); + await settle(4); + } + + // 78 이름 일괄 바꾸기 - 틀 "Show [C]" (첫 번째가 고르지 않은 "Show 01.mkv" 와 겹쳐 빨갛게) + final eps = [p.join(tmp.path, 'Show', 'ep01.mkv'), p.join(tmp.path, 'Show', 'ep02.mkv')]; + showBatchRename(ctx(), eps, isDir: (_) => false); + await settle(); + await t.enterText(find.byType(TextField).first, 'Show [C]'); + await settle(); + await shot('78_rename'); + await closeAll(); + + // 79 찾기 결과 - 하나 골라 아래 할 일 줄까지 + navKey.currentState!.push(MaterialPageRoute( + builder: (_) => SearchResultsPage( + root: tmp.path, + cond: const SearchConditions(name: 'ep'), + showHidden: false, + otherDir: p.join(tmp.path, 'docs'), + onOpen: (_) async {}, + ), + )); + await settle(12); + await t.tap(find.byType(Checkbox).first); + await settle(); + await shot('79_search'); + await closeAll(); + + // 81 빠른 보기 - 자막 (글 미리 보기) + final list = await t.runAsync(() => listEntries(p.join(tmp.path, 'Show'))); + final srt = list!.indexWhere((e) => e.name == 'ep01.srt'); + showDialog( + context: ctx(), + builder: (_) => QuickLook( + entries: list, + index: srt, + probe: (_) async => const MediaInfo(streams: []), + thumbnail: (_) async => null, + imageExts: const ['jpg', 'png'], + onOpen: (_) {}, + ), + ); + await settle(10); + await shot('81_quicklook'); + await closeAll(); + + // 82 놓기 묻기 창 + askDropAction(ctx(), eps, p.join(tmp.path, 'docs')); + await settle(); + await shot('82_drop_ask'); + await closeAll(); + + // 85 작은 표시 - 보통 · 멈춤 + for (final (name, failed) in [('85_chip', false), ('85_chip_failed', true)]) { + navKey.currentState!.push(MaterialPageRoute( + builder: (_) => Scaffold( + body: Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: TransferProgressChip(running: failed ? 0 : 2, percent: failed ? null : 45, failed: failed, onTap: () {}), + ), + ), + ), + )); + await settle(); + await shot(name); + await closeAll(); + } + + // 86 풀기 확인 창 - 밖으로 나가는 경로 · 링크 · 쓸 수 없는 이름 · 덮어쓰기 경고 + final a = Archive() + ..addFile(ArchiveFile.string('ok/a.txt', 'a')) + ..addFile(ArchiveFile.string('../evil.txt', 'x')) + ..addFile(ArchiveFile.string('CON.txt', 'x')) + ..addFile(ArchiveFile.string('lnk', '../x')..mode = 0xA1FF); + final zip = p.join(tmp.path, 'sample.zip'); + File(zip).writeAsBytesSync(ZipEncoder().encode(a)); + extractArchiveFlow(ctx(), c, zip); + await settle(15); + await shot('86_extract_confirm'); + // 같은 이름: 덮어쓰기 → "되돌릴 수 없습니다" + await t.tap(find.byType(DropdownButton)); + await settle(); + await t.tap(find.text(lang == 'ko' ? '덮어쓰기' : (lang == 'en' ? 'Overwrite' : '上書き')).last); + await settle(); + await shot('86_extract_overwrite'); + await closeAll(); + + // 87 분석 화면 - 큰 폴더 · 큰 파일 + navKey.currentState!.push(MaterialPageRoute(builder: (_) => StorageAnalysisPage(root: tmp.path))); + await settle(15); + await shot('87_storage_folders'); + await t.tap(find.byType(ChoiceChip).last); + await settle(); + await shot('87_storage_files'); + await closeAll(); + }); + } + } +} diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index d0f9288..4bf38d3 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -2717,38 +2717,11 @@ class _ExplorerPageState extends State { final pct = run.isEmpty || run.any((j) => j.counting) ? null : (run.fold(0, (a, j) => a + j.overall) / run.length * 100).round(); - return Material( - elevation: 6, - color: failed ? JjColors.danger.withValues(alpha: 0.25) : JjColors.panelHigh, - borderRadius: BorderRadius.circular(18), - child: InkWell( - borderRadius: BorderRadius.circular(18), - onTap: () => c.updateSettings((x) => x.explorerProgressHidden = false), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2, value: run.isEmpty ? 1 : (pct == null ? null : pct / 100)), - ), - const SizedBox(width: 8), - Text( - failed - ? tr('복사 · 이동 멈춤 - 눌러서 보기') - : run.isEmpty - ? tr('복사 · 이동 끝') - : pct == null - ? trf('복사 · 이동 {0}개', [run.length]) - : trf('복사 · 이동 {0}개 · {1}%', [run.length, pct]), - style: const TextStyle(fontSize: 12.5), - ), - const SizedBox(width: 4), - Tooltip(message: tr('진행 패널 보기'), child: const Icon(Icons.expand_less, size: 18)), - ]), - ), - ), - ); + return TransferProgressChip( + running: run.length, + percent: pct, + failed: failed, + onTap: () => c.updateSettings((x) => x.explorerProgressHidden = false)); }, ), ), @@ -4746,6 +4719,50 @@ class _Thumbs { } } +/// 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: [ + SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, value: running == 0 ? 1 : (percent == null ? null : percent! / 100)), + ), + const SizedBox(width: 8), + Text( + failed + ? tr('복사 · 이동 멈춤 - 눌러서 보기') + : running == 0 + ? tr('복사 · 이동 끝') + : percent == null + ? trf('복사 · 이동 {0}개', [running]) + : trf('복사 · 이동 {0}개 · {1}%', [running, percent]), + style: const TextStyle(fontSize: 12.5), + ), + const SizedBox(width: 4), + Tooltip(message: tr('진행 패널 보기'), child: const Icon(Icons.expand_less, size: 18)), + ]), + ), + ), + ); +} + /// 81: 빠른 보기의 큰 썸네일 (가로 640, 같은 jj_thumbs 에 따로 저장). 사용자가 연 것이라 작업 중에도 만든다 class _BigThumbs { static final _cache = >{};