WIP 릴리스 전 점검 (지시 8): DNS 실패 기록에서 사이트 이름 뺌 · archive 고지 · 큰 글자 넘침 3곳 (검토 대기)
- 개인정보: DNS 강제 프록시의 "DNS 실패 (사이트): 까닭" 이 연 사이트 이름을 app.log 에 남겼다 → 까닭 (닿지 않은 DNS 서버) 만.
- 라이선스: ZIP 풀기 · 묶기의 archive 4.3.0 (MIT, Brendan Duncan) 을 두 고지 (Windows · Android) 에 이름으로. 판이 바뀌면 시험이 알림.
- 접근성 (253 시험을 묶음6 새 창으로 넓힘, 412×900 · 900×412 · 글자 1.6배, 누르는 것이 화면 폭 안인지도 봄):
· 78 이름 일괄 창: 대소문자 고르기가 가장 긴 항목 폭이라 창을 넘음 → 한 줄 전체 (isExpanded). 낮은 화면에서 아래가 넘침 → 창 안 스크롤.
"대소문자 무시" 글은 줄 바꿈.
· 86 풀기 확인 창: "같은 이름이 있으면 [고르기]" 가 화면 밖으로 밀림 (넘침 오류 없이 누를 수 없었다) → 자리가 없으면 다음 줄.
· 85 작은 표시: "복사 · 이동 멈춤 - 눌러서 보기" 가 412 를 넘음 → 두 줄까지.
버그 수정 (사용자 동작은 배치만 바뀜). 시험: 253 의 settle 이 시험 시계를 앞으로 돌리게 (열리는 움직임이 끝나야 넘침이 보임).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
parent
0f3133cc68
commit
a6b3f13e62
|
|
@ -172,7 +172,7 @@
|
||||||
"DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "No DNS server address (Settings > Network · DNS)",
|
"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} 의 주소를 찾지 못해 {1}": "The DNS server couldn't find the address of {0}, so {1}",
|
||||||
"DNS 설정: {0}": "DNS setting: {0}",
|
"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 안내 덮개를 띄움 ({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.",
|
"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)",
|
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "View DOC · DOCX · HWP · XLS · XLSX converted to PDF (downloads a converter)",
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
"DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "DNS サーバーのアドレスがありません (環境設定 > ネットワーク・DNS)",
|
"DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "DNS サーバーのアドレスがありません (環境設定 > ネットワーク・DNS)",
|
||||||
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS サーバーが {0} のアドレスを見つけられないため、{1}",
|
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS サーバーが {0} のアドレスを見つけられないため、{1}",
|
||||||
"DNS 설정: {0}": "DNS 設定: {0}",
|
"DNS 설정: {0}": "DNS 設定: {0}",
|
||||||
"DNS 실패 ({0}): {1}": "DNS 失敗 ({0}): {1}",
|
"DNS 실패: {0}": "DNS 失敗: {0}",
|
||||||
"DNS 안내 덮개를 띄움 ({0} · DNS {1})": "ページに DNS 案内を表示 ({0} · DNS {1})",
|
"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 に切り替える] を押してください。",
|
"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 に変換して表示 (変換ツールをダウンロード)",
|
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "DOC・DOCX・HWP・XLS・XLSX を PDF に変換して表示 (変換ツールをダウンロード)",
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
"DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "没有 DNS 服务器地址 (设置 > 网络 · DNS)",
|
"DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "没有 DNS 服务器地址 (设置 > 网络 · DNS)",
|
||||||
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS 服务器找不到 {0} 的地址,因此{1}",
|
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS 服务器找不到 {0} 的地址,因此{1}",
|
||||||
"DNS 설정: {0}": "DNS 设置: {0}",
|
"DNS 설정: {0}": "DNS 设置: {0}",
|
||||||
"DNS 실패 ({0}): {1}": "DNS 失败 ({0}): {1}",
|
"DNS 실패: {0}": "DNS 失败: {0}",
|
||||||
"DNS 안내 덮개를 띄움 ({0} · DNS {1})": "已在页面上显示 DNS 提示 ({0} · DNS {1})",
|
"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]。",
|
"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 查看 (需下载转换器)",
|
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "将 DOC · DOCX · HWP · XLS · XLSX 转为 PDF 查看 (需下载转换器)",
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ WHISPER_CPP_LICENSE.txt · ONNXRUNTIME_LICENSES.txt 는 실행 파일 옆에도
|
||||||
- file_picker MIT https://pub.dev/packages/file_picker
|
- file_picker MIT https://pub.dev/packages/file_picker
|
||||||
- charset Apache-2.0 https://pub.dev/packages/charset
|
- charset Apache-2.0 https://pub.dev/packages/charset
|
||||||
- cp949_codec BSD https://pub.dev/packages/cp949_codec
|
- 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 C++ 런타임 (msvcp140.dll, vcruntime140*.dll)
|
||||||
Microsoft 재배포 가능 파일 (Visual Studio 라이선스의 Distributable Code)
|
Microsoft 재배포 가능 파일 (Visual Studio 라이선스의 Distributable Code)
|
||||||
- 그 밖의 Dart 패키지 라이선스: 프로그램의 "환경 설정 > 라이선스" 화면
|
- 그 밖의 Dart 패키지 라이선스: 프로그램의 "환경 설정 > 라이선스" 화면
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ JJ_MKVMaker (Android) - 포함된 오픈 소스 구성 요소 및 라이선스
|
||||||
판: 1.15.1 (libonnxruntime.so). 원문 (Third Party Notices 포함): [패키지 라이선스] 의 "ONNX Runtime 1.15.1"
|
판: 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
|
- 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
|
- 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 패키지: [패키지 라이선스]
|
- charset (Apache-2.0) · cp949_codec (BSD) · 그 밖의 Dart 패키지: [패키지 라이선스]
|
||||||
|
|
||||||
[앱에 들어 있는 별도 실행 파일 · 라이브러리]
|
[앱에 들어 있는 별도 실행 파일 · 라이브러리]
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,8 @@ class NetGuard {
|
||||||
// 작업 기록은 1분에 한 번 (같은 실패가 줄줄이 나지 않게)
|
// 작업 기록은 1분에 한 번 (같은 실패가 줄줄이 나지 않게)
|
||||||
if (_lastLog == null || now.difference(_lastLog!) >= const Duration(minutes: 1)) {
|
if (_lastLog == null || now.difference(_lastLog!) >= const Duration(minutes: 1)) {
|
||||||
_lastLog = now;
|
_lastLog = now;
|
||||||
log?.call(trf('DNS 실패 ({0}): {1}', [host, e]));
|
// 연 사이트 이름은 작업 기록 (app.log) 에 남기지 않는다 (개인정보) - 닿지 않은 DNS 서버와 까닭만
|
||||||
|
log?.call(trf('DNS 실패: {0}', [e]));
|
||||||
}
|
}
|
||||||
// 248 · 249: 안내는 "닿지 않음" 이 처음 생길 때 한 번 (다시 답한 뒤 또 실패하면 다시). 상태는 [failure] 에 남는다
|
// 248 · 249: 안내는 "닿지 않음" 이 처음 생길 때 한 번 (다시 답한 뒤 또 실패하면 다시). 상태는 [failure] 에 남는다
|
||||||
if (failure.value != null) return;
|
if (failure.value != null) return;
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,9 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
||||||
_placeChoice(ctx, choices, dest, (d) => set(() => dest = d)),
|
_placeChoice(ctx, choices, dest, (d) => set(() => dest = d)),
|
||||||
Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(children: [
|
// 좁은 화면 · 큰 글자면 고르기 상자를 다음 줄로 (412 · 글자 1.6배에서 화면 밖으로 나갔다 - 253 시험)
|
||||||
|
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
Text(tr('같은 이름이 있으면'), style: const TextStyle(fontSize: 13)),
|
Text(tr('같은 이름이 있으면'), style: const TextStyle(fontSize: 13)),
|
||||||
const SizedBox(width: 8),
|
|
||||||
DropdownButton<NameConflict>(
|
DropdownButton<NameConflict>(
|
||||||
value: conflict,
|
value: conflict,
|
||||||
items: [
|
items: [
|
||||||
|
|
|
||||||
|
|
@ -133,84 +133,87 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> {
|
||||||
final canRun = !_loading && !_running && bad == 0 && changed > 0;
|
final canRun = !_loading && !_running && bad == 0 && changed > 0;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(trf('이름 일괄 바꾸기 · {0}개', [widget.paths.length])),
|
title: Text(trf('이름 일괄 바꾸기 · {0}개', [widget.paths.length])),
|
||||||
|
// 낮은 화면 · 큰 글자에서는 창 안이 스크롤 (253 시험: 아래가 넘쳤다). 폭은 고정이라 미리 보기 목록은 그대로 게으르게
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 640,
|
width: 640,
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
child: SingleChildScrollView(
|
||||||
// 이름 틀 + [N] [C] 넣기
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
Row(children: [
|
// 이름 틀 + [N] [C] 넣기
|
||||||
Expanded(
|
Row(children: [
|
||||||
child: TextField(
|
Expanded(
|
||||||
controller: _template,
|
child: TextField(
|
||||||
enabled: !_running,
|
controller: _template,
|
||||||
decoration: InputDecoration(
|
enabled: !_running,
|
||||||
// 폭 412 에서 긴 이름이 잘려 (일본어) 이름은 짧게, 설명은 아래 줄에
|
decoration: InputDecoration(
|
||||||
labelText: tr('이름 틀'),
|
// 폭 412 에서 긴 이름이 잘려 (일본어) 이름은 짧게, 설명은 아래 줄에
|
||||||
helperText: tr('[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]'),
|
labelText: tr('이름 틀'),
|
||||||
helperMaxLines: 3,
|
helperText: tr('[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]'),
|
||||||
isDense: true,
|
helperMaxLines: 3,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
),
|
),
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
const SizedBox(width: 6),
|
OutlinedButton(onPressed: _running ? null : () => _insert('[N]'), child: const Text('[N]')),
|
||||||
OutlinedButton(onPressed: _running ? null : () => _insert('[N]'), child: const Text('[N]')),
|
const SizedBox(width: 4),
|
||||||
const SizedBox(width: 4),
|
OutlinedButton(onPressed: _running ? null : () => _insert('[C]'), child: const Text('[C]')),
|
||||||
OutlinedButton(onPressed: _running ? null : () => _insert('[C]'), child: const Text('[C]')),
|
]),
|
||||||
]),
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 6),
|
Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.end, children: [
|
||||||
Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.end, children: [
|
_num(_start, tr('번호 시작')),
|
||||||
_num(_start, tr('번호 시작')),
|
_num(_step, tr('간격')),
|
||||||
_num(_step, tr('간격')),
|
_num(_digits, tr('자릿수')),
|
||||||
_num(_digits, tr('자릿수')),
|
]),
|
||||||
|
// 대소문자 고르기는 한 줄 전체 (가장 긴 항목 폭이 좁은 화면 · 큰 글자에서 창을 넘었다 - 253 시험)
|
||||||
DropdownButton<String>(
|
DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
value: _case,
|
value: _case,
|
||||||
items: [
|
items: [
|
||||||
DropdownMenuItem(value: 'keep', child: Text(tr('대소문자 그대로'))),
|
DropdownMenuItem(value: 'keep', child: Text(tr('대소문자 그대로'), overflow: TextOverflow.ellipsis)),
|
||||||
DropdownMenuItem(value: 'lower', child: Text(tr('모두 작게 (abc)'))),
|
DropdownMenuItem(value: 'lower', child: Text(tr('모두 작게 (abc)'), overflow: TextOverflow.ellipsis)),
|
||||||
DropdownMenuItem(value: 'upper', child: Text(tr('모두 크게 (ABC)'))),
|
DropdownMenuItem(value: 'upper', child: Text(tr('모두 크게 (ABC)'), overflow: TextOverflow.ellipsis)),
|
||||||
DropdownMenuItem(value: 'title', child: Text(tr('단어 첫 글자만 크게 (Abc)'))),
|
DropdownMenuItem(value: 'title', child: Text(tr('단어 첫 글자만 크게 (Abc)'), overflow: TextOverflow.ellipsis)),
|
||||||
],
|
],
|
||||||
onChanged: _running ? null : (v) => setState(() => _case = v!),
|
onChanged: _running ? null : (v) => setState(() => _case = v!),
|
||||||
),
|
),
|
||||||
]),
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 6),
|
Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||||
Wrap(spacing: 10, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
SizedBox(
|
||||||
SizedBox(
|
width: 200,
|
||||||
width: 200,
|
child: TextField(
|
||||||
child: TextField(
|
controller: _find,
|
||||||
controller: _find,
|
enabled: !_running,
|
||||||
enabled: !_running,
|
decoration: InputDecoration(labelText: tr('찾을 글'), isDense: true),
|
||||||
decoration: InputDecoration(labelText: tr('찾을 글'), isDense: true),
|
onChanged: (_) => setState(() {}),
|
||||||
onChanged: (_) => setState(() {}),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(
|
||||||
SizedBox(
|
width: 200,
|
||||||
width: 200,
|
child: TextField(
|
||||||
child: TextField(
|
controller: _replace,
|
||||||
controller: _replace,
|
enabled: !_running,
|
||||||
enabled: !_running,
|
decoration: InputDecoration(labelText: tr('바꿀 글'), hintText: tr('비우면 지움'), isDense: true),
|
||||||
decoration: InputDecoration(labelText: tr('바꿀 글'), hintText: tr('비우면 지움'), isDense: true),
|
onChanged: (_) => setState(() {}),
|
||||||
onChanged: (_) => setState(() {}),
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
Checkbox(value: _ignoreCase, onChanged: _running ? null : (v) => setState(() => _ignoreCase = v!)),
|
||||||
Checkbox(value: _ignoreCase, onChanged: _running ? null : (v) => setState(() => _ignoreCase = v!)),
|
Flexible(child: Text(tr('대소문자 무시'))),
|
||||||
Text(tr('대소문자 무시')),
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
Text(
|
||||||
Text(
|
_loading
|
||||||
_loading
|
? tr('폴더를 읽는 중…')
|
||||||
? tr('폴더를 읽는 중…')
|
: bad > 0
|
||||||
: bad > 0
|
? trf('문제 {0}개 - 고치기 전에는 바꾸지 않습니다', [bad])
|
||||||
? trf('문제 {0}개 - 고치기 전에는 바꾸지 않습니다', [bad])
|
: trf('바뀌는 것 {0}개', [changed]),
|
||||||
: trf('바뀌는 것 {0}개', [changed]),
|
style: TextStyle(fontSize: 12, color: bad > 0 ? JjColors.danger : JjColors.textDim),
|
||||||
style: TextStyle(fontSize: 12, color: bad > 0 ? JjColors.danger : JjColors.textDim),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(height: 4),
|
// 미리 보기 (창 전체가 스크롤 - 낮은 화면 · 큰 글자에서 아래가 넘쳤다, 253 시험)
|
||||||
// 미리 보기
|
ConstrainedBox(
|
||||||
Flexible(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 280),
|
constraints: const BoxConstraints(maxHeight: 280),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|
@ -241,13 +244,13 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_error != null)
|
||||||
if (_error != null)
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(top: 8),
|
||||||
padding: const EdgeInsets.only(top: 8),
|
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||||
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
),
|
||||||
),
|
]),
|
||||||
]),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: _running ? null : () => Navigator.pop(context), child: Text(tr('취소'))),
|
TextButton(onPressed: _running ? null : () => Navigator.pop(context), child: Text(tr('취소'))),
|
||||||
|
|
|
||||||
|
|
@ -4749,15 +4749,20 @@ class TransferProgressChip extends StatelessWidget {
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, value: running == 0 ? 1 : (percent == null ? null : percent! / 100)),
|
child: CircularProgressIndicator(strokeWidth: 2, value: running == 0 ? 1 : (percent == null ? null : percent! / 100)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
// 좁은 화면 · 큰 글자면 두 줄로 (412 · 글자 1.6배에서 화면을 넘었다 - 253 시험)
|
||||||
failed
|
Flexible(
|
||||||
? tr('복사 · 이동 멈춤 - 눌러서 보기')
|
child: Text(
|
||||||
: running == 0
|
failed
|
||||||
? tr('복사 · 이동 끝')
|
? tr('복사 · 이동 멈춤 - 눌러서 보기')
|
||||||
: percent == null
|
: running == 0
|
||||||
? trf('복사 · 이동 {0}개', [running])
|
? tr('복사 · 이동 끝')
|
||||||
: trf('복사 · 이동 {0}개 · {1}%', [running, percent]),
|
: percent == null
|
||||||
style: const TextStyle(fontSize: 12.5),
|
? trf('복사 · 이동 {0}개', [running])
|
||||||
|
: trf('복사 · 이동 {0}개 · {1}%', [running, percent]),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Tooltip(message: tr('진행 패널 보기'), child: const Icon(Icons.expand_less, size: 18)),
|
Tooltip(message: tr('진행 패널 보기'), child: const Icon(Icons.expand_less, size: 18)),
|
||||||
|
|
|
||||||
|
|
@ -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/desktop_storage_service.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||||
import 'package:jj_mkvmaker/services/platform_services.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/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_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/explorer_search.dart';
|
||||||
import 'package:jj_mkvmaker/ui/home_page.dart';
|
import 'package:jj_mkvmaker/ui/home_page.dart';
|
||||||
import 'package:jj_mkvmaker/ui/monitor_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/settings_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/storage_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
/// 253: 글자를 크게 (기기 설정 글꼴 크기 · 접근성) 해도 화면이 넘치지 않는다 - 줄 높이가 고정이던 목록이 넘쳤다
|
/// 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<NavigatorState>();
|
||||||
|
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<void> settle([int n = 10]) async {
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
|
||||||
|
// 화면 · 창이 열리는 움직임이 끝나게 (시험 시계를 앞으로)
|
||||||
|
await tester.pump(const Duration(milliseconds: 60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void>(
|
||||||
|
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<void>(
|
||||||
|
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<void>(
|
||||||
|
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<NameConflict>));
|
||||||
|
await tester.pump();
|
||||||
|
// 고르기 상자가 화면 안에 다 있어야 한다 (넘침 오류 없이 화면 밖으로 밀려났었다)
|
||||||
|
expect(tester.getRect(find.byType(DropdownButton<NameConflict>)).right, lessThanOrEqualTo(size.width));
|
||||||
|
await tester.tap(find.byType(DropdownButton<NameConflict>));
|
||||||
|
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<void>(builder: (_) => StorageAnalysisPage(root: tmp.path)));
|
||||||
|
await settle(20);
|
||||||
|
await tester.tap(find.byType(ChoiceChip).last);
|
||||||
|
await check('87 저장 공간 분석 (큰 파일)');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,12 @@ void main() {
|
||||||
}
|
}
|
||||||
expect(File('tool/build_7zz_android.sh').readAsStringSync(), contains('2aed39b8f1238464475e9de7dda169a5e873a1dc8bbf4f664b943eaba5620181'));
|
expect(File('tool/build_7zz_android.sh').readAsStringSync(), contains('2aed39b8f1238464475e9de7dda169a5e873a1dc8bbf4f664b943eaba5620181'));
|
||||||
expect(File('android/app/third_party_libs.txt').readAsLinesSync(), contains('lib7zz.so'));
|
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)', () {
|
test('Windows 설치: 실행 파일 옆 · tools · rsync 에 원문을 둔다 (CMake)', () {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue