diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 30dc83e..e92ab26 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -172,7 +172,7 @@ "DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "No DNS server address (Settings > Network · DNS)", "DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "The DNS server couldn't find the address of {0}, so {1}", "DNS 설정: {0}": "DNS setting: {0}", - "DNS 실패 ({0}): {1}": "DNS failure ({0}): {1}", + "DNS 실패: {0}": "DNS failure: {0}", "DNS 안내 덮개를 띄움 ({0} · DNS {1})": "Showed DNS notice over the page ({0} · DNS {1})", "DNS: 기기 DNS → 직접 지정 (8.8.8.8 · 8.8.4.4). 앱의 인터넷 연결 (브라우저 · 다운로드 · 업데이트) 은 이 서버로 주소를 찾습니다. 되돌리기: 환경 설정 > 네트워크 · DNS > 시스템 DNS (끄기) 회사 · 학교망처럼 8.8.8.8 이 막힌 곳에서는 닿지 않는다는 안내의 [시스템 DNS 로 바꾸기] 를 누르세요.": "DNS: device DNS → custom (8.8.8.8 · 8.8.4.4). The app's internet connections (browser · downloads · updates) now look up addresses with these servers. To undo: Settings > Network · DNS > System DNS (off) On networks that block 8.8.8.8 (such as company or school networks), press [Switch to system DNS] in the \"can't reach\" notice.", "DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "View DOC · DOCX · HWP · XLS · XLSX converted to PDF (downloads a converter)", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index cdbc1dd..ed9d71b 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -172,7 +172,7 @@ "DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "DNS サーバーのアドレスがありません (環境設定 > ネットワーク・DNS)", "DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS サーバーが {0} のアドレスを見つけられないため、{1}", "DNS 설정: {0}": "DNS 設定: {0}", - "DNS 실패 ({0}): {1}": "DNS 失敗 ({0}): {1}", + "DNS 실패: {0}": "DNS 失敗: {0}", "DNS 안내 덮개를 띄움 ({0} · DNS {1})": "ページに DNS 案内を表示 ({0} · DNS {1})", "DNS: 기기 DNS → 직접 지정 (8.8.8.8 · 8.8.4.4). 앱의 인터넷 연결 (브라우저 · 다운로드 · 업데이트) 은 이 서버로 주소를 찾습니다. 되돌리기: 환경 설정 > 네트워크 · DNS > 시스템 DNS (끄기) 회사 · 학교망처럼 8.8.8.8 이 막힌 곳에서는 닿지 않는다는 안내의 [시스템 DNS 로 바꾸기] 를 누르세요.": "DNS: 端末の DNS → 直接指定 (8.8.8.8 · 8.8.4.4)。アプリのインターネット接続 (ブラウザ · ダウンロード · 更新) はこのサーバーでアドレスを調べます。元に戻す: 設定 > ネットワーク · DNS > システム DNS (オフ) 会社 · 学校のネットワークなど 8.8.8.8 がブロックされている所では、接続できないという案内の [システム DNS に切り替える] を押してください。", "DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "DOC・DOCX・HWP・XLS・XLSX を PDF に変換して表示 (変換ツールをダウンロード)", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 8eb62c4..0d736d4 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -172,7 +172,7 @@ "DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "没有 DNS 服务器地址 (设置 > 网络 · DNS)", "DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS 服务器找不到 {0} 的地址,因此{1}", "DNS 설정: {0}": "DNS 设置: {0}", - "DNS 실패 ({0}): {1}": "DNS 失败 ({0}): {1}", + "DNS 실패: {0}": "DNS 失败: {0}", "DNS 안내 덮개를 띄움 ({0} · DNS {1})": "已在页面上显示 DNS 提示 ({0} · DNS {1})", "DNS: 기기 DNS → 직접 지정 (8.8.8.8 · 8.8.4.4). 앱의 인터넷 연결 (브라우저 · 다운로드 · 업데이트) 은 이 서버로 주소를 찾습니다. 되돌리기: 환경 설정 > 네트워크 · DNS > 시스템 DNS (끄기) 회사 · 학교망처럼 8.8.8.8 이 막힌 곳에서는 닿지 않는다는 안내의 [시스템 DNS 로 바꾸기] 를 누르세요.": "DNS:设备 DNS → 手动指定 (8.8.8.8 · 8.8.4.4)。应用的网络连接 (浏览器 · 下载 · 更新) 将用这些服务器查找地址。恢复:设置 > 网络 · DNS > 系统 DNS (关闭) 在公司 · 学校等屏蔽 8.8.8.8 的网络中,请点击无法连接提示中的 [切换为系统 DNS]。", "DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "将 DOC · DOCX · HWP · XLS · XLSX 转为 PDF 查看 (需下载转换器)", diff --git a/assets/licenses/THIRD_PARTY_NOTICES.txt b/assets/licenses/THIRD_PARTY_NOTICES.txt index 18258f3..7c93a8a 100644 --- a/assets/licenses/THIRD_PARTY_NOTICES.txt +++ b/assets/licenses/THIRD_PARTY_NOTICES.txt @@ -48,6 +48,8 @@ WHISPER_CPP_LICENSE.txt · ONNXRUNTIME_LICENSES.txt 는 실행 파일 옆에도 - file_picker MIT https://pub.dev/packages/file_picker - charset Apache-2.0 https://pub.dev/packages/charset - cp949_codec BSD https://pub.dev/packages/cp949_codec +- archive 4.3.0 (ZIP 풀기 · 묶기) MIT https://pub.dev/packages/archive + Copyright (c) 2013-2021 Brendan Duncan. - Microsoft Visual C++ 런타임 (msvcp140.dll, vcruntime140*.dll) Microsoft 재배포 가능 파일 (Visual Studio 라이선스의 Distributable Code) - 그 밖의 Dart 패키지 라이선스: 프로그램의 "환경 설정 > 라이선스" 화면 diff --git a/assets/licenses/THIRD_PARTY_NOTICES_ANDROID.txt b/assets/licenses/THIRD_PARTY_NOTICES_ANDROID.txt index 3c4eeea..d57fc66 100644 --- a/assets/licenses/THIRD_PARTY_NOTICES_ANDROID.txt +++ b/assets/licenses/THIRD_PARTY_NOTICES_ANDROID.txt @@ -29,6 +29,8 @@ JJ_MKVMaker (Android) - 포함된 오픈 소스 구성 요소 및 라이선스 판: 1.15.1 (libonnxruntime.so). 원문 (Third Party Notices 포함): [패키지 라이선스] 의 "ONNX Runtime 1.15.1" - flutter_inappwebview (JJ_MKVMaker 수정본) Apache-2.0 https://pub.dev/packages/flutter_inappwebview - file_picker · android_file_picker MIT https://pub.dev/packages/file_picker +- archive 4.3.0 (ZIP 풀기 · 묶기) MIT https://pub.dev/packages/archive + Copyright (c) 2013-2021 Brendan Duncan. - charset (Apache-2.0) · cp949_codec (BSD) · 그 밖의 Dart 패키지: [패키지 라이선스] [앱에 들어 있는 별도 실행 파일 · 라이브러리] diff --git a/lib/app/net_guard.dart b/lib/app/net_guard.dart index 4861fe6..8d3ea4c 100644 --- a/lib/app/net_guard.dart +++ b/lib/app/net_guard.dart @@ -86,7 +86,8 @@ class NetGuard { // 작업 기록은 1분에 한 번 (같은 실패가 줄줄이 나지 않게) if (_lastLog == null || now.difference(_lastLog!) >= const Duration(minutes: 1)) { _lastLog = now; - log?.call(trf('DNS 실패 ({0}): {1}', [host, e])); + // 연 사이트 이름은 작업 기록 (app.log) 에 남기지 않는다 (개인정보) - 닿지 않은 DNS 서버와 까닭만 + log?.call(trf('DNS 실패: {0}', [e])); } // 248 · 249: 안내는 "닿지 않음" 이 처음 생길 때 한 번 (다시 답한 뒤 또 실패하면 다시). 상태는 [failure] 에 남는다 if (failure.value != null) return; diff --git a/lib/ui/archive_ui.dart b/lib/ui/archive_ui.dart index 96bb6d9..b6f65a3 100644 --- a/lib/ui/archive_ui.dart +++ b/lib/ui/archive_ui.dart @@ -204,9 +204,9 @@ Future extractArchiveFlow(BuildContext context, AppController c, String _placeChoice(ctx, choices, dest, (d) => set(() => dest = d)), Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)), const SizedBox(height: 6), - Row(children: [ + // 좁은 화면 · 큰 글자면 고르기 상자를 다음 줄로 (412 · 글자 1.6배에서 화면 밖으로 나갔다 - 253 시험) + Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text(tr('같은 이름이 있으면'), style: const TextStyle(fontSize: 13)), - const SizedBox(width: 8), DropdownButton( value: conflict, items: [ diff --git a/lib/ui/batch_rename_dialog.dart b/lib/ui/batch_rename_dialog.dart index 88d8ef9..e55b980 100644 --- a/lib/ui/batch_rename_dialog.dart +++ b/lib/ui/batch_rename_dialog.dart @@ -133,84 +133,87 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> { final canRun = !_loading && !_running && bad == 0 && changed > 0; return AlertDialog( title: Text(trf('이름 일괄 바꾸기 · {0}개', [widget.paths.length])), + // 낮은 화면 · 큰 글자에서는 창 안이 스크롤 (253 시험: 아래가 넘쳤다). 폭은 고정이라 미리 보기 목록은 그대로 게으르게 content: SizedBox( width: 640, - child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // 이름 틀 + [N] [C] 넣기 - Row(children: [ - Expanded( - child: TextField( - controller: _template, - enabled: !_running, - decoration: InputDecoration( - // 폭 412 에서 긴 이름이 잘려 (일본어) 이름은 짧게, 설명은 아래 줄에 - labelText: tr('이름 틀'), - helperText: tr('[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]'), - helperMaxLines: 3, - isDense: true, + child: SingleChildScrollView( + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // 이름 틀 + [N] [C] 넣기 + Row(children: [ + Expanded( + child: TextField( + controller: _template, + enabled: !_running, + decoration: InputDecoration( + // 폭 412 에서 긴 이름이 잘려 (일본어) 이름은 짧게, 설명은 아래 줄에 + labelText: tr('이름 틀'), + helperText: tr('[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]'), + helperMaxLines: 3, + isDense: true, + ), + onChanged: (_) => setState(() {}), ), - onChanged: (_) => setState(() {}), ), - ), - const SizedBox(width: 6), - OutlinedButton(onPressed: _running ? null : () => _insert('[N]'), child: const Text('[N]')), - const SizedBox(width: 4), - OutlinedButton(onPressed: _running ? null : () => _insert('[C]'), child: const Text('[C]')), - ]), - const SizedBox(height: 6), - Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.end, children: [ - _num(_start, tr('번호 시작')), - _num(_step, tr('간격')), - _num(_digits, tr('자릿수')), + const SizedBox(width: 6), + OutlinedButton(onPressed: _running ? null : () => _insert('[N]'), child: const Text('[N]')), + const SizedBox(width: 4), + OutlinedButton(onPressed: _running ? null : () => _insert('[C]'), child: const Text('[C]')), + ]), + const SizedBox(height: 6), + Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.end, children: [ + _num(_start, tr('번호 시작')), + _num(_step, tr('간격')), + _num(_digits, tr('자릿수')), + ]), + // 대소문자 고르기는 한 줄 전체 (가장 긴 항목 폭이 좁은 화면 · 큰 글자에서 창을 넘었다 - 253 시험) DropdownButton( + isExpanded: true, value: _case, items: [ - DropdownMenuItem(value: 'keep', child: Text(tr('대소문자 그대로'))), - DropdownMenuItem(value: 'lower', child: Text(tr('모두 작게 (abc)'))), - DropdownMenuItem(value: 'upper', child: Text(tr('모두 크게 (ABC)'))), - DropdownMenuItem(value: 'title', child: Text(tr('단어 첫 글자만 크게 (Abc)'))), + DropdownMenuItem(value: 'keep', child: Text(tr('대소문자 그대로'), overflow: TextOverflow.ellipsis)), + DropdownMenuItem(value: 'lower', child: Text(tr('모두 작게 (abc)'), overflow: TextOverflow.ellipsis)), + DropdownMenuItem(value: 'upper', child: Text(tr('모두 크게 (ABC)'), overflow: TextOverflow.ellipsis)), + DropdownMenuItem(value: 'title', child: Text(tr('단어 첫 글자만 크게 (Abc)'), overflow: TextOverflow.ellipsis)), ], onChanged: _running ? null : (v) => setState(() => _case = v!), ), - ]), - const SizedBox(height: 6), - Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ - SizedBox( - width: 200, - child: TextField( - controller: _find, - enabled: !_running, - decoration: InputDecoration(labelText: tr('찾을 글'), isDense: true), - onChanged: (_) => setState(() {}), + const SizedBox(height: 6), + Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ + SizedBox( + width: 200, + child: TextField( + controller: _find, + enabled: !_running, + decoration: InputDecoration(labelText: tr('찾을 글'), isDense: true), + onChanged: (_) => setState(() {}), + ), ), - ), - SizedBox( - width: 200, - child: TextField( - controller: _replace, - enabled: !_running, - decoration: InputDecoration(labelText: tr('바꿀 글'), hintText: tr('비우면 지움'), isDense: true), - onChanged: (_) => setState(() {}), + SizedBox( + width: 200, + child: TextField( + controller: _replace, + enabled: !_running, + decoration: InputDecoration(labelText: tr('바꿀 글'), hintText: tr('비우면 지움'), isDense: true), + onChanged: (_) => setState(() {}), + ), ), - ), - Row(mainAxisSize: MainAxisSize.min, children: [ - Checkbox(value: _ignoreCase, onChanged: _running ? null : (v) => setState(() => _ignoreCase = v!)), - Text(tr('대소문자 무시')), + Row(mainAxisSize: MainAxisSize.min, children: [ + Checkbox(value: _ignoreCase, onChanged: _running ? null : (v) => setState(() => _ignoreCase = v!)), + Flexible(child: Text(tr('대소문자 무시'))), + ]), ]), - ]), - const SizedBox(height: 10), - Text( - _loading - ? tr('폴더를 읽는 중…') - : bad > 0 - ? trf('문제 {0}개 - 고치기 전에는 바꾸지 않습니다', [bad]) - : trf('바뀌는 것 {0}개', [changed]), - style: TextStyle(fontSize: 12, color: bad > 0 ? JjColors.danger : JjColors.textDim), - ), - const SizedBox(height: 4), - // 미리 보기 - Flexible( - child: ConstrainedBox( + const SizedBox(height: 10), + Text( + _loading + ? tr('폴더를 읽는 중…') + : bad > 0 + ? trf('문제 {0}개 - 고치기 전에는 바꾸지 않습니다', [bad]) + : trf('바뀌는 것 {0}개', [changed]), + style: TextStyle(fontSize: 12, color: bad > 0 ? JjColors.danger : JjColors.textDim), + ), + const SizedBox(height: 4), + // 미리 보기 (창 전체가 스크롤 - 낮은 화면 · 큰 글자에서 아래가 넘쳤다, 253 시험) + ConstrainedBox( constraints: const BoxConstraints(maxHeight: 280), child: ListView.builder( shrinkWrap: true, @@ -241,13 +244,13 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> { }, ), ), - ), - if (_error != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)), - ), - ]), + if (_error != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)), + ), + ]), + ), ), actions: [ TextButton(onPressed: _running ? null : () => Navigator.pop(context), child: Text(tr('취소'))), diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 3667cb9..b2ecd21 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -4749,15 +4749,20 @@ class TransferProgressChip extends StatelessWidget { 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), + // 좁은 화면 · 큰 글자면 두 줄로 (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)), diff --git a/test/large_text_test.dart b/test/large_text_test.dart index 2f42c37..5f44b3a 100644 --- a/test/large_text_test.dart +++ b/test/large_text_test.dart @@ -10,11 +10,19 @@ 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:archive/archive_io.dart'; +import 'package:jj_mkvmaker/core/file_ops.dart'; +import 'package:jj_mkvmaker/ui/archive_ui.dart'; +import 'package:jj_mkvmaker/ui/batch_rename_dialog.dart'; import 'package:jj_mkvmaker/ui/downloads_page.dart'; +import 'package:jj_mkvmaker/ui/explorer_drag.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; +import 'package:jj_mkvmaker/ui/explorer_search.dart'; import 'package:jj_mkvmaker/ui/home_page.dart'; import 'package:jj_mkvmaker/ui/monitor_page.dart'; +import 'package:jj_mkvmaker/ui/quick_look.dart'; import 'package:jj_mkvmaker/ui/settings_page.dart'; +import 'package:jj_mkvmaker/ui/storage_page.dart'; import 'package:path/path.dart' as p; /// 253: 글자를 크게 (기기 설정 글꼴 크기 · 접근성) 해도 화면이 넘치지 않는다 - 줄 높이가 고정이던 목록이 넘쳤다 @@ -66,4 +74,99 @@ void main() { }); } } + + // 관리자 지시 8 (묶음6 릴리스 전 점검): 새 창 · 화면도 폭 412 · 글자 1.6배에서 넘치지 않는다 + for (final size in [const Size(412, 900), const Size(900, 412)]) { + testWidgets('253: 묶음6 새 창 (78 · 79 · 81 · 82 · 85 · 86 · 87, ${size.width.round()}×${size.height.round()}, 글자 1.6배) 넘치지 않는다', + (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final show = Directory(p.join(tmp.path, '아주 긴 이름의 폴더 - 시험용', 'Show'))..createSync(recursive: true); + final eps = [ + for (final n in ['아주 긴 이름의 동영상 파일 - 시험용 2026 ep01.mkv', 'ep02.mkv']) + (File(p.join(show.path, n))..writeAsBytesSync(List.filled(3000, 0))).path, + ]; + File(p.join(show.path, 'ep01.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕\n'); + final zip = p.join(tmp.path, '아주 긴 이름의 압축 파일 - 시험용.zip'); + File(zip).writeAsBytesSync(ZipEncoder().encode(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 c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final nav = GlobalKey(); + await tester.pumpWidget(MaterialApp( + navigatorKey: nav, + builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: const TextScaler.linear(1.6)), child: child!), + home: const Scaffold(body: SizedBox.expand()))); + BuildContext ctx() => nav.currentContext!; + Future settle([int n = 10]) async { + for (var i = 0; i < n; i++) { + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); + // 화면 · 창이 열리는 움직임이 끝나게 (시험 시계를 앞으로) + await tester.pump(const Duration(milliseconds: 60)); + } + } + + Future check(String what) async { + await settle(); + expect(tester.takeException(), isNull, reason: what); + // 넘침 오류 없이 옆으로 밀려나는 것도 본다: 누르는 것 (단추 · 고르기 · 입력 칸) 은 화면 폭 안에 + final hit = find.byWidgetPredicate((w) => + w is ButtonStyleButton || w is IconButton || w is Checkbox || w is DropdownButton || w is TextField || w is ChoiceChip || w is Radio); + for (final e in hit.evaluate()) { + final r = tester.getRect(find.byElementPredicate((x) => x == e)); + expect(r.left >= -0.5 && r.right <= size.width + 0.5, isTrue, reason: '$what: ${e.widget.runtimeType} $r'); + } + nav.currentState!.popUntil((r) => r.isFirst); + await settle(4); + } + + showBatchRename(ctx(), eps, isDir: (_) => false); + await check('78 이름 일괄'); + nav.currentState!.push(MaterialPageRoute( + builder: (_) => SearchResultsPage( + root: tmp.path, cond: const SearchConditions(name: 'ep', minBytes: 1024), showHidden: false, otherDir: tmp.path, onOpen: (_) async {}))); + await settle(15); + await tester.tap(find.byType(Checkbox).first); + await check('79 찾기 결과 (고른 뒤 아래 줄)'); + final list = await tester.runAsync(() => listEntries(show.path)); + showDialog( + context: ctx(), + builder: (_) => QuickLook( + entries: list!, + index: list.indexWhere((e) => e.name == 'ep01.srt'), + probe: (_) async => const MediaInfo(streams: []), + thumbnail: (_) async => null, + imageExts: const ['jpg'], + onOpen: (_) {})); + await check('81 빠른 보기'); + askDropAction(ctx(), eps, tmp.path); + await check('82 놓기 묻기'); + nav.currentState!.push(MaterialPageRoute( + builder: (_) => Scaffold( + body: Align( + alignment: Alignment.bottomRight, + child: TransferProgressChip(running: 0, failed: true, onTap: () {}))))); + await check('85 멈춤 표시'); + extractArchiveFlow(ctx(), c, zip, otherDir: show.path); + await settle(20); + // 글자가 크면 창 아래쪽이라 내려서 누른다 (창은 스크롤) + await tester.ensureVisible(find.byType(DropdownButton)); + await tester.pump(); + // 고르기 상자가 화면 안에 다 있어야 한다 (넘침 오류 없이 화면 밖으로 밀려났었다) + expect(tester.getRect(find.byType(DropdownButton)).right, lessThanOrEqualTo(size.width)); + await tester.tap(find.byType(DropdownButton)); + await settle(4); + await tester.tap(find.text('덮어쓰기').last); + await check('86 풀기 확인 (경고 줄 · 덮어쓰기)'); + createZipFlow(ctx(), c, eps, dir: show.path, otherDir: tmp.path); + await check('86 ZIP 묶기'); + nav.currentState!.push(MaterialPageRoute(builder: (_) => StorageAnalysisPage(root: tmp.path))); + await settle(20); + await tester.tap(find.byType(ChoiceChip).last); + await check('87 저장 공간 분석 (큰 파일)'); + }); + } } diff --git a/test/licenses_test.dart b/test/licenses_test.dart index 34fd901..94b70a7 100644 --- a/test/licenses_test.dart +++ b/test/licenses_test.dart @@ -103,6 +103,12 @@ void main() { } expect(File('tool/build_7zz_android.sh').readAsStringSync(), contains('2aed39b8f1238464475e9de7dda169a5e873a1dc8bbf4f664b943eaba5620181')); expect(File('android/app/third_party_libs.txt').readAsLinesSync(), contains('lib7zz.so')); + // ZIP 풀기 · 묶기는 archive 패키지 (MIT) - 두 고지에 이름으로 + for (final t in [notices, android]) { + expect(t, contains('archive 4.3.0')); + } + expect(File('pubspec.lock').readAsStringSync(), matches(RegExp(r'\n archive:\r?\n(?: .*\r?\n){1,8}? version: "4\.3\.0"')), + reason: 'archive 판이 바뀌면 고지의 판 번호도 고친다'); }); test('Windows 설치: 실행 파일 옆 · tools · rsync 에 원문을 둔다 (CMake)', () {