WIP 옵션 칸 보충 (관리자 지시 10/20, 28 지적): --cc · --zc · --zl · --zt 등 값 받는 별칭 (오탐) · -–delete 처럼 하이픈과 대시가 섞인 것도 고침 · 안내 끝 문구를 문제 종류에 맞게 (따옴표 닫기 · - 로 시작 · 값 적기 · 곧은 글자로) · 옵션 낱말은 줄바꿈 없이 · 캡처 시험 windows_option_problem_shot_test (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>next9
parent
f45ad3b25f
commit
b1a43c36c5
|
|
@ -657,6 +657,7 @@
|
||||||
"감지한 언어: {0} (자동 감지)": "Detected language: {0} (auto-detect)",
|
"감지한 언어: {0} (자동 감지)": "Detected language: {0} (auto-detect)",
|
||||||
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "Detected language: {0} - if it is wrong, make them again with another language",
|
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "Detected language: {0} - if it is wrong, make them again with another language",
|
||||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "Downloaded only if the detected language differs from the subtitle language {0}",
|
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "Downloaded only if the detected language differs from the subtitle language {0}",
|
||||||
|
"값을 적어 주세요 (예: --exclude=*.tmp)": "Add a value (e.g. --exclude=*.tmp)",
|
||||||
"강하게 (0.4)": "Strong (0.4)",
|
"강하게 (0.4)": "Strong (0.4)",
|
||||||
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "Stronger refines more but may drift from the original",
|
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "Stronger refines more but may drift from the original",
|
||||||
"같습니다 ✔": "Match ✔",
|
"같습니다 ✔": "Match ✔",
|
||||||
|
|
@ -751,6 +752,7 @@
|
||||||
"고치기": "Edit",
|
"고치기": "Edit",
|
||||||
"고친 옵션을 저장하지 않았습니다": "The edited options aren't saved",
|
"고친 옵션을 저장하지 않았습니다": "The edited options aren't saved",
|
||||||
"고화질": "High quality",
|
"고화질": "High quality",
|
||||||
|
"곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요": "Retype with straight quotes and hyphens (-)",
|
||||||
"골라서 시작": "Choose and start",
|
"골라서 시작": "Choose and start",
|
||||||
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "Cannot roll back: shared storage (Download) is not available.",
|
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "Cannot roll back: shared storage (Download) is not available.",
|
||||||
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "Files downloaded to shared folders (videos, images, etc.)",
|
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "Files downloaded to shared folders (videos, images, etc.)",
|
||||||
|
|
@ -1131,6 +1133,7 @@
|
||||||
"등록됨: \"JJ_MKVMaker 로 재생\" · \"JJ_MKVMaker 로 자막 만들기\" (Windows 11: 더 많은 옵션 표시)": "Registered: \"Play with JJ_MKVMaker\" · \"Make subtitles with JJ_MKVMaker\" (Windows 11: Show more options)",
|
"등록됨: \"JJ_MKVMaker 로 재생\" · \"JJ_MKVMaker 로 자막 만들기\" (Windows 11: 더 많은 옵션 표시)": "Registered: \"Play with JJ_MKVMaker\" · \"Make subtitles with JJ_MKVMaker\" (Windows 11: Show more options)",
|
||||||
"따르지 않는 것: Tor 화면 (이름도 Tor 안에서 찾음), IP 주소, 같은 집 안 이름 (점 없는 NAS · PC 이름, *.local, Tailscale 의 *.ts.net 등은 기기 DNS)": "Not covered: the Tor screen (names are looked up inside Tor), IP addresses, home-network names (NAS / PC names without a dot, *.local, Tailscale *.ts.net etc. use the device DNS)",
|
"따르지 않는 것: Tor 화면 (이름도 Tor 안에서 찾음), IP 주소, 같은 집 안 이름 (점 없는 NAS · PC 이름, *.local, Tailscale 의 *.ts.net 등은 기기 DNS)": "Not covered: the Tor screen (names are looked up inside Tor), IP addresses, home-network names (NAS / PC names without a dot, *.local, Tailscale *.ts.net etc. use the device DNS)",
|
||||||
"따옴표가 닫히지 않았습니다: {0}": "Unclosed quote: {0}",
|
"따옴표가 닫히지 않았습니다: {0}": "Unclosed quote: {0}",
|
||||||
|
"따옴표를 닫아 주세요": "Close the quote",
|
||||||
"또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)": "Or scan the QR code with the other device's camera (it includes the code)",
|
"또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)": "Or scan the QR code with the other device's camera (it includes the code)",
|
||||||
"라오어": "Lao",
|
"라오어": "Lao",
|
||||||
"라이선스 원문 보기": "View full license text",
|
"라이선스 원문 보기": "View full license text",
|
||||||
|
|
@ -1892,8 +1895,8 @@
|
||||||
"옮길 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to move (tap, or mark with the circle on the right).",
|
"옮길 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to move (tap, or mark with the circle on the right).",
|
||||||
"옮길까요?": "move?",
|
"옮길까요?": "move?",
|
||||||
"옵션": "Options",
|
"옵션": "Options",
|
||||||
"옵션을 알아볼 수 없습니다: {0}": "Unrecognized option: {0}",
|
"옵션은 - 나 -- 로 시작합니다": "Options start with - or --",
|
||||||
"옵션을 알아볼 수 없습니다: {0} - 곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요": "Unrecognized option: {0} - retype it with straight quotes and hyphens (-)",
|
"옵션을 알아볼 수 없습니다: {0} - {1}": "Unrecognized option: {0} - {1}",
|
||||||
"옵션을 저장했습니다": "Options saved",
|
"옵션을 저장했습니다": "Options saved",
|
||||||
"완료": "Done",
|
"완료": "Done",
|
||||||
"완료 시 자동 동영상 추가": "Auto-add videos when done",
|
"완료 시 자동 동영상 추가": "Auto-add videos when done",
|
||||||
|
|
|
||||||
|
|
@ -657,6 +657,7 @@
|
||||||
"감지한 언어: {0} (자동 감지)": "検出した言語: {0} (自動検出)",
|
"감지한 언어: {0} (자동 감지)": "検出した言語: {0} (自動検出)",
|
||||||
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "検出した言語: {0} - 違っていれば別の言語で作り直してください",
|
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "検出した言語: {0} - 違っていれば別の言語で作り直してください",
|
||||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "検出した言語が作成する言語と異なる場合のみダウンロード {0}",
|
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "検出した言語が作成する言語と異なる場合のみダウンロード {0}",
|
||||||
|
"값을 적어 주세요 (예: --exclude=*.tmp)": "値を入力してください (例: --exclude=*.tmp)",
|
||||||
"강하게 (0.4)": "強め (0.4)",
|
"강하게 (0.4)": "強め (0.4)",
|
||||||
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "強いほど大きく整えますが、元と違ってくることがあります",
|
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "強いほど大きく整えますが、元と違ってくることがあります",
|
||||||
"같습니다 ✔": "一致します ✔",
|
"같습니다 ✔": "一致します ✔",
|
||||||
|
|
@ -751,6 +752,7 @@
|
||||||
"고치기": "編集",
|
"고치기": "編集",
|
||||||
"고친 옵션을 저장하지 않았습니다": "編集したオプションが保存されていません",
|
"고친 옵션을 저장하지 않았습니다": "編集したオプションが保存されていません",
|
||||||
"고화질": "高画質",
|
"고화질": "高画質",
|
||||||
|
"곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요": "半角の引用符とハイフン (-) で入力し直してください",
|
||||||
"골라서 시작": "選んで開始",
|
"골라서 시작": "選んで開始",
|
||||||
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "共有ストレージ (Download) を使えないため戻せません。",
|
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "共有ストレージ (Download) を使えないため戻せません。",
|
||||||
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "共有フォルダーにダウンロードしたファイル (動画・画像など)",
|
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "共有フォルダーにダウンロードしたファイル (動画・画像など)",
|
||||||
|
|
@ -1131,6 +1133,7 @@
|
||||||
"등록됨: \"JJ_MKVMaker 로 재생\" · \"JJ_MKVMaker 로 자막 만들기\" (Windows 11: 더 많은 옵션 표시)": "登録済み:「JJ_MKVMaker で再生」・「JJ_MKVMaker で字幕を作成」(Windows 11: その他のオプションを表示)",
|
"등록됨: \"JJ_MKVMaker 로 재생\" · \"JJ_MKVMaker 로 자막 만들기\" (Windows 11: 더 많은 옵션 표시)": "登録済み:「JJ_MKVMaker で再生」・「JJ_MKVMaker で字幕を作成」(Windows 11: その他のオプションを表示)",
|
||||||
"따르지 않는 것: Tor 화면 (이름도 Tor 안에서 찾음), IP 주소, 같은 집 안 이름 (점 없는 NAS · PC 이름, *.local, Tailscale 의 *.ts.net 등은 기기 DNS)": "対象外: Tor 画面 (名前も Tor 内で解決)、IP アドレス、家庭内ネットワークの名前 (ドットのない NAS・PC 名、*.local、Tailscale の *.ts.net などは端末の DNS)",
|
"따르지 않는 것: Tor 화면 (이름도 Tor 안에서 찾음), IP 주소, 같은 집 안 이름 (점 없는 NAS · PC 이름, *.local, Tailscale 의 *.ts.net 등은 기기 DNS)": "対象外: Tor 画面 (名前も Tor 内で解決)、IP アドレス、家庭内ネットワークの名前 (ドットのない NAS・PC 名、*.local、Tailscale の *.ts.net などは端末の DNS)",
|
||||||
"따옴표가 닫히지 않았습니다: {0}": "引用符が閉じていません: {0}",
|
"따옴표가 닫히지 않았습니다: {0}": "引用符が閉じていません: {0}",
|
||||||
|
"따옴표를 닫아 주세요": "引用符を閉じてください",
|
||||||
"또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)": "または、もう一方の端末のカメラで QR コードを読み取ってください (コード入り)",
|
"또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)": "または、もう一方の端末のカメラで QR コードを読み取ってください (コード入り)",
|
||||||
"라오어": "ラオ語",
|
"라오어": "ラオ語",
|
||||||
"라이선스 원문 보기": "ライセンス原文を見る",
|
"라이선스 원문 보기": "ライセンス原文を見る",
|
||||||
|
|
@ -1892,8 +1895,8 @@
|
||||||
"옮길 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "移動する項目を選んでください (タップするか右の丸で印を付ける)。",
|
"옮길 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "移動する項目を選んでください (タップするか右の丸で印を付ける)。",
|
||||||
"옮길까요?": "移動しますか?",
|
"옮길까요?": "移動しますか?",
|
||||||
"옵션": "オプション",
|
"옵션": "オプション",
|
||||||
"옵션을 알아볼 수 없습니다: {0}": "オプションを認識できません: {0}",
|
"옵션은 - 나 -- 로 시작합니다": "オプションは - か -- で始まります",
|
||||||
"옵션을 알아볼 수 없습니다: {0} - 곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요": "オプションを認識できません: {0} - 半角の引用符とハイフン (-) で入力し直してください",
|
"옵션을 알아볼 수 없습니다: {0} - {1}": "オプションを認識できません: {0} - {1}",
|
||||||
"옵션을 저장했습니다": "オプションを保存しました",
|
"옵션을 저장했습니다": "オプションを保存しました",
|
||||||
"완료": "完了",
|
"완료": "完了",
|
||||||
"완료 시 자동 동영상 추가": "完了時に動画を自動追加",
|
"완료 시 자동 동영상 추가": "完了時に動画を自動追加",
|
||||||
|
|
|
||||||
|
|
@ -657,6 +657,7 @@
|
||||||
"감지한 언어: {0} (자동 감지)": "检测到的语言:{0}(自动检测)",
|
"감지한 언어: {0} (자동 감지)": "检测到的语言:{0}(自动检测)",
|
||||||
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "检测到的语言:{0} - 如有误,请用其他语言重新生成",
|
"감지한 언어: {0} - 틀렸으면 다른 언어로 다시 만드세요": "检测到的语言:{0} - 如有误,请用其他语言重新生成",
|
||||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "仅当检测到的语言与要生成的语言不同时下载 {0}",
|
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "仅当检测到的语言与要生成的语言不同时下载 {0}",
|
||||||
|
"값을 적어 주세요 (예: --exclude=*.tmp)": "请填写值 (例: --exclude=*.tmp)",
|
||||||
"강하게 (0.4)": "强 (0.4)",
|
"강하게 (0.4)": "强 (0.4)",
|
||||||
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "越强优化越多,但可能与原图不同",
|
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "越强优化越多,但可能与原图不同",
|
||||||
"같습니다 ✔": "相同 ✔",
|
"같습니다 ✔": "相同 ✔",
|
||||||
|
|
@ -751,6 +752,7 @@
|
||||||
"고치기": "编辑",
|
"고치기": "编辑",
|
||||||
"고친 옵션을 저장하지 않았습니다": "修改的选项尚未保存",
|
"고친 옵션을 저장하지 않았습니다": "修改的选项尚未保存",
|
||||||
"고화질": "高画质",
|
"고화질": "高画质",
|
||||||
|
"곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요": "请用直引号和连字符 (-) 重新输入",
|
||||||
"골라서 시작": "选择后开始",
|
"골라서 시작": "选择后开始",
|
||||||
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "无法使用公共存储 (Download),因此无法回退。",
|
"공용 저장소 (Download) 를 쓸 수 없어 되돌릴 수 없습니다.": "无法使用公共存储 (Download),因此无法回退。",
|
||||||
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "下载到公共文件夹的文件 (视频、图片等)",
|
"공용 폴더에 받은 파일 (동영상 · 그림 등)": "下载到公共文件夹的文件 (视频、图片等)",
|
||||||
|
|
@ -1131,6 +1133,7 @@
|
||||||
"등록됨: \"JJ_MKVMaker 로 재생\" · \"JJ_MKVMaker 로 자막 만들기\" (Windows 11: 더 많은 옵션 표시)": "已注册: “用 JJ_MKVMaker 播放” · “用 JJ_MKVMaker 生成字幕”(Windows 11: 显示更多选项)",
|
"등록됨: \"JJ_MKVMaker 로 재생\" · \"JJ_MKVMaker 로 자막 만들기\" (Windows 11: 더 많은 옵션 표시)": "已注册: “用 JJ_MKVMaker 播放” · “用 JJ_MKVMaker 生成字幕”(Windows 11: 显示更多选项)",
|
||||||
"따르지 않는 것: Tor 화면 (이름도 Tor 안에서 찾음), IP 주소, 같은 집 안 이름 (점 없는 NAS · PC 이름, *.local, Tailscale 의 *.ts.net 등은 기기 DNS)": "不适用: Tor 画面 (名称也在 Tor 内解析)、IP 地址、家庭网络内的名称 (不带点的 NAS · PC 名称、*.local、Tailscale 的 *.ts.net 等使用设备 DNS)",
|
"따르지 않는 것: Tor 화면 (이름도 Tor 안에서 찾음), IP 주소, 같은 집 안 이름 (점 없는 NAS · PC 이름, *.local, Tailscale 의 *.ts.net 등은 기기 DNS)": "不适用: Tor 画面 (名称也在 Tor 内解析)、IP 地址、家庭网络内的名称 (不带点的 NAS · PC 名称、*.local、Tailscale 的 *.ts.net 等使用设备 DNS)",
|
||||||
"따옴표가 닫히지 않았습니다: {0}": "引号未闭合: {0}",
|
"따옴표가 닫히지 않았습니다: {0}": "引号未闭合: {0}",
|
||||||
|
"따옴표를 닫아 주세요": "请闭合引号",
|
||||||
"또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)": "或用另一台设备的相机扫描二维码 (已包含代码)",
|
"또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)": "或用另一台设备的相机扫描二维码 (已包含代码)",
|
||||||
"라오어": "老挝语",
|
"라오어": "老挝语",
|
||||||
"라이선스 원문 보기": "查看许可证原文",
|
"라이선스 원문 보기": "查看许可证原文",
|
||||||
|
|
@ -1892,8 +1895,8 @@
|
||||||
"옮길 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要移动的项目 (点按或用右侧圆圈标记)。",
|
"옮길 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要移动的项目 (点按或用右侧圆圈标记)。",
|
||||||
"옮길까요?": "移动吗?",
|
"옮길까요?": "移动吗?",
|
||||||
"옵션": "选项",
|
"옵션": "选项",
|
||||||
"옵션을 알아볼 수 없습니다: {0}": "无法识别的选项: {0}",
|
"옵션은 - 나 -- 로 시작합니다": "选项以 - 或 -- 开头",
|
||||||
"옵션을 알아볼 수 없습니다: {0} - 곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요": "无法识别的选项: {0} - 请用直引号和连字符 (-) 重新输入",
|
"옵션을 알아볼 수 없습니다: {0} - {1}": "无法识别的选项: {0} - {1}",
|
||||||
"옵션을 저장했습니다": "已保存选项",
|
"옵션을 저장했습니다": "已保存选项",
|
||||||
"완료": "完成",
|
"완료": "完成",
|
||||||
"완료 시 자동 동영상 추가": "完成后自动添加视频",
|
"완료 시 자동 동영상 추가": "完成后自动添加视频",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
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/i18n_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/sync_tools.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/sync_preview_view.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 관리자 지시 (10/20, 28 지적): 알아볼 수 없는 옵션 안내 - 끝 문구가 문제 종류에 맞는지, 412 에서 넘치지 않는지.
|
||||||
|
/// 확인 창의 빨간 글 (OptionProblemNote) 과 설정 칸 아래 글 (errorText, 3줄까지) 을 크기 412 · 960, 언어 ko · en · ja 로 찍는다.
|
||||||
|
/// 실행: flutter test integration_test\windows_option_problem_shot_test.dart -d windows (JJ_SHOT_DIR 에 PNG)
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_optproblem');
|
||||||
|
const cases = [
|
||||||
|
'-a --delete --exclude="a.tmp --exclude=keep.txt', // 닫히지 않은 따옴표
|
||||||
|
'-a delete', // 경로로 읽힘
|
||||||
|
'-a --delete --exclude', // 값 없음
|
||||||
|
'-a ---delete «x»', // 이상한 글자
|
||||||
|
'-av delete --exclude', // 여러 종류
|
||||||
|
];
|
||||||
|
for (final lang in ['ko', 'en', 'ja']) {
|
||||||
|
for (final (w, hh) in [(412, 900), (960, 600)]) {
|
||||||
|
testWidgets('옵션 안내 $lang ${w}x$hh', (t) async {
|
||||||
|
t.view.physicalSize = Size(w.toDouble(), hh.toDouble());
|
||||||
|
t.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(t.view.reset);
|
||||||
|
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||||
|
addTearDown(() => i18n.apply('ko', save: false));
|
||||||
|
final shotKey = GlobalKey();
|
||||||
|
await t.pumpWidget(RepaintBoundary(
|
||||||
|
key: shotKey,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: buildTheme(),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: Scaffold(
|
||||||
|
body: ListView(padding: const EdgeInsets.all(16), children: [
|
||||||
|
for (final o in cases) ...[
|
||||||
|
Text(o, style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||||
|
OptionProblemNote(optionProblems(o)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
// 설정 칸 (copy_sync_settings 의 rsync 옵션 칸과 같은 폭 · 글 줄 수)
|
||||||
|
SizedBox(
|
||||||
|
width: 380,
|
||||||
|
child: TextField(
|
||||||
|
controller: TextEditingController(text: cases.last),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: optionProblemText(optionProblems(cases.last)),
|
||||||
|
errorMaxLines: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 60)));
|
||||||
|
await t.pump(const Duration(milliseconds: 60));
|
||||||
|
}
|
||||||
|
expect(t.takeException(), isNull, reason: '넘침');
|
||||||
|
expect(find.byType(OptionProblemNote), findsNWidgets(cases.length));
|
||||||
|
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, 'optproblem_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -511,13 +511,13 @@ class LiveSync extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 rsync 옵션 (있으면 맞추지 않는다 - 카드에 빨갛게)
|
/// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 rsync 옵션 (있으면 맞추지 않는다 - 카드에 빨갛게)
|
||||||
List<String> unreadableOf(LiveSyncPair x) => unreadableOptions(_optionsOf(x));
|
List<OptionProblem> unreadableOf(LiveSyncPair x) => optionProblems(_optionsOf(x));
|
||||||
|
|
||||||
Future<void> syncNow(LiveSyncPair x) async {
|
Future<void> syncNow(LiveSyncPair x) async {
|
||||||
final k = keyOf(x);
|
final k = keyOf(x);
|
||||||
final bad = unreadableOf(x);
|
final bad = unreadableOf(x);
|
||||||
final nest = bad.isNotEmpty
|
final nest = bad.isNotEmpty
|
||||||
? trf('옵션을 알아볼 수 없습니다: {0}', [bad.join(' · ')])
|
? optionProblemMessage(bad)
|
||||||
: nestingProblem(x, allowInnerToOuter: c.settings.allowInnerToOuter);
|
: nestingProblem(x, allowInnerToOuter: c.settings.allowInnerToOuter);
|
||||||
if (nest != null) {
|
if (nest != null) {
|
||||||
status[k] = (DateTime.now(), nest);
|
status[k] = (DateTime.now(), nest);
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,8 @@ class TransferJob extends ChangeNotifier {
|
||||||
try {
|
try {
|
||||||
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션이 남으면 실행하지 않는다 (rsync 가 원본 경로로 읽어 옵션이 몰래 빠짐)
|
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션이 남으면 실행하지 않는다 (rsync 가 원본 경로로 읽어 옵션이 몰래 빠짐)
|
||||||
if (method == CopyMethod.rsync) {
|
if (method == CopyMethod.rsync) {
|
||||||
final bad = unreadableOptions(options);
|
final bad = optionProblems(options);
|
||||||
if (bad.isNotEmpty) throw StateError(trf('옵션을 알아볼 수 없습니다: {0}', [bad.join(' · ')]));
|
if (bad.isNotEmpty) throw StateError(optionProblemMessage(bad));
|
||||||
}
|
}
|
||||||
for (var i = 0; i < sources.length; i++) {
|
for (var i = 0; i < sources.length; i++) {
|
||||||
final s = sources[i];
|
final s = sources[i];
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,8 @@ List<String> _normalizeWords(List<_Word> raw) {
|
||||||
value = '';
|
value = '';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!w.quoted && t.isNotEmpty && _dashLike.contains(t[0])) {
|
// 28 확인 (10/20): 곧은 하이픈과 섞인 것 (-–delete) 도 맨 앞 둘 중 하나라도 대시면 같은 규칙으로
|
||||||
|
if (!w.quoted && t.isNotEmpty && (_dashLike.contains(t[0]) || t.length > 1 && t[0] == '-' && _dashLike.contains(t[1]))) {
|
||||||
final n = _dashAt(t, 1) ? 2 : 1;
|
final n = _dashAt(t, 1) ? 2 : 1;
|
||||||
final rest = t.substring(n);
|
final rest = t.substring(n);
|
||||||
final name = rest.split('=').first;
|
final name = rest.split('=').first;
|
||||||
|
|
@ -190,15 +191,51 @@ const _rsyncLongWithValue = {
|
||||||
'--block-size', '--checksum-choice', '--compress-choice', '--compress-level', '--skip-compress', '--info', '--debug',
|
'--block-size', '--checksum-choice', '--compress-choice', '--compress-level', '--skip-compress', '--info', '--debug',
|
||||||
'--sockopts', '--port', '--address', '--outbuf', '--remote-option', '--max-alloc', '--stop-after', '--stop-at',
|
'--sockopts', '--port', '--address', '--outbuf', '--remote-option', '--max-alloc', '--stop-after', '--stop-at',
|
||||||
'--write-batch', '--only-write-batch', '--read-batch', '--protocol', '--checksum-seed', '--copy-as', '--early-input',
|
'--write-batch', '--only-write-batch', '--read-batch', '--protocol', '--checksum-seed', '--copy-as', '--early-input',
|
||||||
|
// 28 확인 (10/20): rsync --help 의 짧은 별칭 (aka --cc · --zc · --zl · --zt) 과 값 받는 옵션 - 빠져 있어 값이 "경로로 읽힙니다" 로 막혔다
|
||||||
|
'--cc', '--zc', '--zl', '--zt', '--compress-threads', '--confine-root',
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 관리자 지시 (10/20, 28 · 15 확인): [splitOptions] 로 고쳐 읽은 뒤에도 rsync 가 다르게 읽을 것 - 있으면 확인 창 · 실시간 동기화에
|
/// 관리자 지시 (10/20, 28 · 15 확인): [splitOptions] 로 고쳐 읽은 뒤에도 rsync 가 다르게 읽을 것 - 있으면 확인 창 · 실시간 동기화에
|
||||||
/// 빨갛게 알리고 실행을 막는다: 닫히지 않은 따옴표, 옵션 자리의 - 로 시작하지 않는 낱말 (rsync 는 원본 경로로 읽어 옵션이 몰래 빠짐),
|
/// 빨갛게 알리고 실행을 막는다: 닫히지 않은 따옴표, 옵션 자리의 - 로 시작하지 않는 낱말 (rsync 는 원본 경로로 읽어 옵션이 몰래 빠짐),
|
||||||
/// 하이픈 셋 이상, 끝에 값 없이 남은 값 받는 옵션. 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다
|
/// 하이픈 셋 이상, 끝에 값 없이 남은 값 받는 옵션. 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다
|
||||||
List<String> unreadableOptions(String options) {
|
List<String> unreadableOptions(String options) => [for (final x in optionProblems(options)) x.text];
|
||||||
|
|
||||||
|
/// 옵션 칸의 문제 종류 - 안내의 끝 문구 (고칠 방법) 를 고른다 (관리자 지시 10/20, 28 지적)
|
||||||
|
enum OptionProblemKind { unclosed, path, noValue, odd }
|
||||||
|
|
||||||
|
/// [unreadableOptions] 의 한 줄과 그 종류
|
||||||
|
class OptionProblem {
|
||||||
|
final OptionProblemKind kind;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
/// [text] 안의 옵션 낱말 (화면이 줄바꿈 없이 묶는다)
|
||||||
|
final String token;
|
||||||
|
const OptionProblem(this.kind, this.text, [this.token = '']);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 문제 종류에 맞는 고칠 방법 (여러 종류면 · 로 잇는다)
|
||||||
|
String optionProblemHint(Iterable<OptionProblem> problems) {
|
||||||
|
final kinds = {for (final x in problems) x.kind};
|
||||||
|
return [
|
||||||
|
if (kinds.contains(OptionProblemKind.unclosed)) tr('따옴표를 닫아 주세요'),
|
||||||
|
if (kinds.contains(OptionProblemKind.path)) tr('옵션은 - 나 -- 로 시작합니다'),
|
||||||
|
if (kinds.contains(OptionProblemKind.noValue)) tr('값을 적어 주세요 (예: --exclude=*.tmp)'),
|
||||||
|
if (kinds.contains(OptionProblemKind.odd)) tr('곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요'),
|
||||||
|
].join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "옵션을 알아볼 수 없습니다: … - 고칠 방법" (작업 기록 · 실시간 동기화 카드 · 설정 칸)
|
||||||
|
String optionProblemMessage(List<OptionProblem> problems) =>
|
||||||
|
trf('옵션을 알아볼 수 없습니다: {0} - {1}', [problems.join(' · '), optionProblemHint(problems)]);
|
||||||
|
|
||||||
|
/// [unreadableOptions] 의 몸통 (종류와 함께)
|
||||||
|
List<OptionProblem> optionProblems(String options) {
|
||||||
final parsed = _parseOptions(options);
|
final parsed = _parseOptions(options);
|
||||||
// 15 확인 F1: 닫히지 않은 따옴표 - 뒤의 옵션이 다르게 읽힐 수 있다
|
// 15 확인 F1: 닫히지 않은 따옴표 - 뒤의 옵션이 다르게 읽힐 수 있다
|
||||||
final bad = [for (final u in parsed.unclosed) trf('따옴표가 닫히지 않았습니다: {0}', [u])];
|
final bad = [for (final u in parsed.unclosed) OptionProblem(OptionProblemKind.unclosed, trf('따옴표가 닫히지 않았습니다: {0}', [u]), u)];
|
||||||
var value = false;
|
var value = false;
|
||||||
var last = '';
|
var last = '';
|
||||||
for (final t in parsed.words) {
|
for (final t in parsed.words) {
|
||||||
|
|
@ -208,7 +245,7 @@ List<String> unreadableOptions(String options) {
|
||||||
}
|
}
|
||||||
if (t.startsWith('---')) {
|
if (t.startsWith('---')) {
|
||||||
// 관리자 판단 (10/20, 28 확인): 하이픈이 셋 이상 (---delete) 은 rsync 가 알아보지 못한다
|
// 관리자 판단 (10/20, 28 확인): 하이픈이 셋 이상 (---delete) 은 rsync 가 알아보지 못한다
|
||||||
bad.add(t);
|
bad.add(OptionProblem(OptionProblemKind.odd, t, t));
|
||||||
} else if (t.startsWith('--')) {
|
} else if (t.startsWith('--')) {
|
||||||
value = !t.contains('=') && _rsyncLongWithValue.contains(t);
|
value = !t.contains('=') && _rsyncLongWithValue.contains(t);
|
||||||
last = t;
|
last = t;
|
||||||
|
|
@ -220,11 +257,12 @@ List<String> unreadableOptions(String options) {
|
||||||
} else {
|
} else {
|
||||||
// 관리자 판단 (10/20, 28 확인): 옵션 칸에는 경로가 들어갈 자리가 없다 (원본 · 대상은 앱이 넣음) - 값 자리가 아닌데
|
// 관리자 판단 (10/20, 28 확인): 옵션 칸에는 경로가 들어갈 자리가 없다 (원본 · 대상은 앱이 넣음) - 값 자리가 아닌데
|
||||||
// - 로 시작하지 않는 낱말 (-a delete 의 delete, 혼자 쓴 -) 은 rsync 가 원본 경로로 읽어 옵션이 몰래 빠진다
|
// - 로 시작하지 않는 낱말 (-a delete 의 delete, 혼자 쓴 -) 은 rsync 가 원본 경로로 읽어 옵션이 몰래 빠진다
|
||||||
bad.add(trf('경로로 읽힙니다: {0}', [t]));
|
// ASCII 밖 글자로 시작하면 (« … 등) 붙여 넣은 이상한 글자 - 안내는 곧은 글자로 다시 적기
|
||||||
|
bad.add(OptionProblem(t.codeUnitAt(0) > 0x7F ? OptionProblemKind.odd : OptionProblemKind.path, trf('경로로 읽힙니다: {0}', [t]), t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 값이 필요한 옵션이 끝에 값 없이 (--exclude) - rsync 가 원본 경로를 그 값으로 먹는다
|
// 값이 필요한 옵션이 끝에 값 없이 (--exclude) - rsync 가 원본 경로를 그 값으로 먹는다
|
||||||
if (value) bad.add(trf('{0} 의 값이 없습니다', [last]));
|
if (value) bad.add(OptionProblem(OptionProblemKind.noValue, trf('{0} 의 값이 없습니다', [last]), last));
|
||||||
return bad;
|
return bad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import 'rsync_setup.dart';
|
||||||
import 'schedule_editor.dart';
|
import 'schedule_editor.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'setting_tile.dart';
|
import 'setting_tile.dart';
|
||||||
import 'sync_preview_view.dart' show OptionTextFormatter;
|
import 'sync_preview_view.dart' show OptionTextFormatter, optionProblemText;
|
||||||
|
|
||||||
/// 환경 설정의 복사 · 동기화 부분.
|
/// 환경 설정의 복사 · 동기화 부분.
|
||||||
/// - 파일 탐색기 ([rsync] false): 파일 / 폴더 복사 방법 (현재 방식 · robocopy), robocopy 옵션, 속도 제한
|
/// - 파일 탐색기 ([rsync] false): 파일 / 폴더 복사 방법 (현재 방식 · robocopy), robocopy 옵션, 속도 제한
|
||||||
|
|
@ -79,7 +79,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
||||||
Widget _options(String title, String hint, String value, String def, void Function(AppSettings, String) set, {bool rsync = false}) {
|
Widget _options(String title, String hint, String value, String def, void Function(AppSettings, String) set, {bool rsync = false}) {
|
||||||
final ctl = _optionText.putIfAbsent(title, () => TextEditingController(text: value));
|
final ctl = _optionText.putIfAbsent(title, () => TextEditingController(text: value));
|
||||||
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 rsync 옵션은 칸 아래에 빨갛게 (실행 · 실시간 동기화는 막힘)
|
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 rsync 옵션은 칸 아래에 빨갛게 (실행 · 실시간 동기화는 막힘)
|
||||||
final bad = rsync ? unreadableOptions(ctl.text) : const <String>[];
|
final bad = rsync ? optionProblems(ctl.text) : const <OptionProblem>[];
|
||||||
return SettingTile(
|
return SettingTile(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
subtitle: Text(hint),
|
subtitle: Text(hint),
|
||||||
|
|
@ -93,7 +93,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: bad.isEmpty ? null : trf('옵션을 알아볼 수 없습니다: {0}', [bad.join(' · ')]),
|
errorText: bad.isEmpty ? null : optionProblemText(bad),
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
tooltip: trf('기본값으로 ({0})', [def]),
|
tooltip: trf('기본값으로 ({0})', [def]),
|
||||||
|
|
|
||||||
|
|
@ -3419,7 +3419,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
// 관리자 지시 (10/19): 원격 (앱이 맞춤) 도 같은 거르기를 따르므로 미리 볼 수 없는 규칙도 똑같이 알린다
|
// 관리자 지시 (10/19): 원격 (앱이 맞춤) 도 같은 거르기를 따르므로 미리 볼 수 없는 규칙도 똑같이 알린다
|
||||||
List<String> filterLimits() => {for (final t in tasks) ...RsyncFilter.parse(opts(t)).unsupported}.toList();
|
List<String> filterLimits() => {for (final t in tasks) ...RsyncFilter.parse(opts(t)).unsupported}.toList();
|
||||||
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션이 남으면 [실행] 을 막는다
|
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션이 남으면 [실행] 을 막는다
|
||||||
List<String> badOptions() => {for (final t in tasks) ...unreadableOptions(opts(t))}.toList();
|
List<OptionProblem> badOptions() {
|
||||||
|
final seen = <String>{};
|
||||||
|
return [for (final t in tasks) ...optionProblems(opts(t)).where((x) => seen.add(x.text))];
|
||||||
|
}
|
||||||
// 93: 지우는 실행 (--delete 등. 100: WebDAV 도 따른다) 은 비교가 끝나 지울 목록이 보인 뒤에만
|
// 93: 지우는 실행 (--delete 등. 100: WebDAV 도 따른다) 은 비교가 끝나 지울 목록이 보인 뒤에만
|
||||||
bool deletes() => tasks.any((t) => optionsDelete(opts(t)));
|
bool deletes() => tasks.any((t) => optionsDelete(opts(t)));
|
||||||
// 105: 원본이 대상 안인데 지우기가 들어가면 막는다 - 창 안에서 옵션이 바뀔 때 (원본 파일 지우기) 와 실행 직전에도 본다
|
// 105: 원본이 대상 안인데 지우기가 들어가면 막는다 - 창 안에서 옵션이 바뀔 때 (원본 파일 지우기) 와 실행 직전에도 본다
|
||||||
|
|
|
||||||
|
|
@ -663,7 +663,7 @@ class _CopyCardState extends State<_CopyCard> {
|
||||||
style: TextStyle(fontSize: 12, color: t.move ? Colors.orangeAccent : JjColors.textDim),
|
style: TextStyle(fontSize: 12, color: t.move ? Colors.orangeAccent : JjColors.textDim),
|
||||||
),
|
),
|
||||||
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 (실행은 작업이 막는다)
|
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 (실행은 작업이 막는다)
|
||||||
if (_method == 'rsync' && unreadableOptions(_options.text).isNotEmpty) OptionProblemNote(unreadableOptions(_options.text)),
|
if (_method == 'rsync' && optionProblems(_options.text).isNotEmpty) OptionProblemNote(optionProblems(_options.text)),
|
||||||
if (!t.move && optionsRemoveSource(_options.text))
|
if (!t.move && optionsRemoveSource(_options.text))
|
||||||
Text(tr('복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요'),
|
Text(tr('복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요'),
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
|
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
||||||
// 177: rsync 로 맞출 때만 옵션의 거르기 (--exclude 등) 를 비교에도 (앱이 직접 · robocopy 는 이 규칙을 쓰지 않음)
|
// 177: rsync 로 맞출 때만 옵션의 거르기 (--exclude 등) 를 비교에도 (앱이 직접 · robocopy 는 이 규칙을 쓰지 않음)
|
||||||
// 관리자 지시 (10/19): 원격 (앱이 맞춤) 도 rsync 를 고른 작업이면 같은 거르기를 따른다 (현재 방식 · robocopy 는 이 규칙을 쓰지 않음)
|
// 관리자 지시 (10/19): 원격 (앱이 맞춤) 도 rsync 를 고른 작업이면 같은 거르기를 따른다 (현재 방식 · robocopy 는 이 규칙을 쓰지 않음)
|
||||||
final filter = t.method == 'rsync' ? RsyncFilter.parse(t.options) : RsyncFilter.none;
|
final filter = t.method == 'rsync' ? RsyncFilter.parse(t.options) : RsyncFilter.none;
|
||||||
final bad = t.method == 'rsync' ? unreadableOptions(t.options) : const <String>[];
|
final bad = t.method == 'rsync' ? optionProblems(t.options) : const <OptionProblem>[];
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
|
@ -335,15 +335,26 @@ class OptionTextFormatter extends TextInputFormatter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 - 빨갛게 알리고 [실행] 을 막는다 (rsync 가 원본 경로로 읽어 옵션이 몰래 빠짐)
|
/// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 - 빨갛게 알리고 [실행] 을 막는다 (rsync 가 원본 경로로 읽어 옵션이 몰래 빠짐)
|
||||||
|
/// 알아볼 수 없는 옵션 안내 글 (확인 창 · 설정 칸): 옵션 낱말은 줄바꿈 없이 묶고 (28 지적: "--" 와 "exclude" 로 갈림),
|
||||||
|
/// 고칠 방법의 - · -- 뒤에서도 줄을 바꾸지 않는다 (나머지 글은 보통처럼 줄을 바꾼다)
|
||||||
|
String optionProblemText(List<OptionProblem> bad) {
|
||||||
|
final wj = String.fromCharCode(0x2060);
|
||||||
|
return trf('옵션을 알아볼 수 없습니다: {0} - {1}', [
|
||||||
|
bad.map((x) => x.token.isEmpty ? x.text : x.text.replaceFirst(x.token, unbreakableOption(x.token))).join(' · '),
|
||||||
|
optionProblemHint(bad).replaceAll('-', '-$wj'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
class OptionProblemNote extends StatelessWidget {
|
class OptionProblemNote extends StatelessWidget {
|
||||||
final List<String> bad;
|
final List<OptionProblem> bad;
|
||||||
const OptionProblemNote(this.bad, {super.key});
|
const OptionProblemNote(this.bad, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Padding(
|
Widget build(BuildContext context) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
trf('옵션을 알아볼 수 없습니다: {0} - 곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요', [bad.map(unbreakableOption).join(' · ')]),
|
// 28 지적 (10/20): 끝 문구는 문제 종류에 맞게 (따옴표 닫기 · - 로 시작 · 값 적기 · 곧은 글자로)
|
||||||
|
optionProblemText(bad),
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)),
|
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,28 @@ void main() {
|
||||||
expect(unreadableOptions('-a --exclude *.log --delete'), isEmpty, reason: '값이 있음');
|
expect(unreadableOptions('-a --exclude *.log --delete'), isEmpty, reason: '값이 있음');
|
||||||
expect(unreadableOptions('-a --exclude=*.log'), isEmpty);
|
expect(unreadableOptions('-a --exclude=*.log'), isEmpty);
|
||||||
expect(unreadableOptions(''), isEmpty);
|
expect(unreadableOptions(''), isEmpty);
|
||||||
|
// 28 확인 (10/20): rsync 의 짧은 별칭 (--cc · --zc · --zl) 도 값을 받는다 (오탐이던 것)
|
||||||
|
expect(unreadableOptions('-a --cc md5 --zc zlib --zl 3 --zt 2'), isEmpty);
|
||||||
|
// 곧은 하이픈과 대시가 섞인 것 (-\u2013delete)
|
||||||
|
expect(splitOptions('-a -\u2013delete \u2013-del'), ['-a', '--delete', '--del']);
|
||||||
|
expect(unreadableOptions('-a -\u2013delete'), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('28 지적 (10/20): 안내 끝 문구는 문제 종류에 맞게', () {
|
||||||
|
String hint(String o) => optionProblemHint(optionProblems(o));
|
||||||
|
expect(hint('-a --exclude="a.tmp'), '따옴표를 닫아 주세요');
|
||||||
|
expect(hint('-a delete'), '옵션은 - 나 -- 로 시작합니다');
|
||||||
|
expect(hint('-a --exclude'), '값을 적어 주세요 (예: --exclude=*.tmp)');
|
||||||
|
expect(hint('-a ---delete'), '곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요');
|
||||||
|
expect(hint('-a \u00ABx\u00BB'), '곧은 따옴표와 하이픈 (-) 으로 다시 적어 주세요');
|
||||||
|
expect(hint('-a delete --exclude'), '옵션은 - 나 -- 로 시작합니다 · 값을 적어 주세요 (예: --exclude=*.tmp)');
|
||||||
|
expect(optionProblemMessage(optionProblems('-a delete')), '옵션을 알아볼 수 없습니다: 경로로 읽힙니다: delete - 옵션은 - 나 -- 로 시작합니다');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('알아볼 수 없는 옵션 알림 (빨간 글, 옵션 글은 줄바꿈 없이)', (tester) async {
|
testWidgets('알아볼 수 없는 옵션 알림 (빨간 글, 옵션 글은 줄바꿈 없이)', (tester) async {
|
||||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: OptionProblemNote(['\u00ABdelete\u00BB']))));
|
await tester.pumpWidget(MaterialApp(home: Scaffold(body: OptionProblemNote(optionProblems('-a \u00ABdelete\u00BB')))));
|
||||||
expect(find.textContaining('옵션을 알아볼 수 없습니다'), findsOneWidget);
|
expect(find.textContaining('옵션을 알아볼 수 없습니다'), findsOneWidget);
|
||||||
|
expect(find.textContaining('곧은 따옴표와 하이픈'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 28 확인 (10/20): 막기 전 실제 rsync 는 이런 낱말을 원본 경로로 먹는다 - 그래서 [unreadableOptions] 로 막는다
|
// 28 확인 (10/20): 막기 전 실제 rsync 는 이런 낱말을 원본 경로로 먹는다 - 그래서 [unreadableOptions] 로 막는다
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue