F3 복사 · 이동 끝 알림을 문장으로 잇기 · F4 키 암호 안내 "위의 [개인 키 암호] 칸" · 185 캡처 시험 (관리자 지시 10/21, 15 찾음)

- F3: "1개 항목을 옮겼습니다. · 1개는 원본에 남음 (건너뜀)" 처럼 마침표 뒤에 " · " 가 붙었다 → 뒤 조각도 문장 ("1개는 원본에 남았습니다 (건너뜀).",
  "받는 쪽이 더 새것이라 n개를 건너뛰었습니다." 등 5개) 으로 쓰고 빈칸으로 잇기 (en · ja · zh). 같은 꼴을 찾아보니 다른 끝 알림 (지우기 ·
  되돌리기) 은 이미 빈칸으로 이어 그대로. 옛 조각 글은 작업 기록 · 모니터링 · 확인 창에서도 써 열쇠는 남김
- F4: 키를 고른 뒤 안내가 창 맨 아래인데 "아래에 키 암호를" 이었다 → "위의 [개인 키 암호] 칸에 키 암호를 넣으세요" (칸 이름은 실제 칸 이름)
- 185: windows_dlcancel_shot_test - [다운로드 취소] 묻는 창 → [계속 받기] 면 그대로 받음 (가짜 백엔드), ko · en · ja × 412 · 960.
  정식 판 후보 1b31609 로 6개 통과 (release\test\shots_v20261020_001 dlcancel_*), after_1020 으로도 6개 통과
- 확인: analyze · l10n 빠진 것 0, explorer_conflict · move_settled · sftp 48개, moveleft · sshkey 캡처 통과 (release\test\shots_f1),
  ja 960 이동 알림 · en 412 키 안내 · ja 412 취소 창 직접 봄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2
RyuJongJin 2026-10-21 11:25:43 +09:00
parent 51f16d5548
commit 616878d885
9 changed files with 139 additions and 16 deletions

View File

@ -583,6 +583,7 @@
"{0}개 항목을 휴지통으로 보낼까요?": "Move {0} items to the Recycle Bin?",
"{0}개 항목을 휴지통으로 보냈습니다.": "Moved {0} items to the Recycle Bin.",
"{0}개는 더 새 파일이라 그대로 둠": "{0} kept (existing file is newer)",
"{0}개는 원본에 남았습니다 (건너뜀).": "{0} left in the source (skipped).",
"{0}개는 원본에 남음 (건너뜀)": "{0} left in the source (skipped)",
"{0}개는 지우지 않았습니다 (취소).": "{0} were not deleted (cancelled).",
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} were too large for the Recycle Bin and were deleted permanently (as Windows asked).",
@ -673,6 +674,7 @@
"같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)",
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} files that replaced a file with the same name won't be undone.",
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "Can't create the folder because a file with the same name exists: {0}",
"같은 이름의 폴더가 있어 {0}개를 건너뛰었습니다: {1}.": "Skipped {0} because a folder with the same name exists: {1}.",
"같은 이름의 폴더가 있어 건너뜀 {0}": "Skipped {0} (a folder with the same name exists)",
"같은 이름의 폴더가 있어 건너뜀 {0}개: {1}": "Skipped {0} because a folder with the same name exists: {1}",
"같은 이름이 이미 있습니다": "An item with the same name already exists",
@ -681,6 +683,7 @@
"같은 이름이 있는지 확인하는 중 …": "Checking for items with the same name …",
"같은 이름이 있으면": "If the name exists",
"같은 이름이 있을 때": "When a file with the same name exists",
"같은 이름이라 건너뛰었습니다: {0}.": "Skipped (same name): {0}.",
"같은 이름이라 건너뜀 {0}개": "{0} skipped (same name)",
"같은 이름이라 건너뜀: {0}": "Skipped (same name): {0}",
"같은 조건으로 다시 (시드 그대로)": "Again with the same settings (same seed)",
@ -702,7 +705,7 @@
"개인 키 파일 고르기": "Choose private key file",
"개인 키 파일이 아닙니다 (너무 큼)": "This isn't a private key file (too large)",
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "Private key added (the original file path isn't saved)",
"개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "Private key added. It is protected by a passphrase, so enter the key passphrase below (the original file path is not saved)",
"개인 키를 넣었습니다. 암호가 걸린 키라 위의 [{0}] 칸에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "Private key added. It is protected by a passphrase, so enter it in the [{0}] field above (the original file path is not saved)",
"개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)": "Can't read the private key. Choose an OpenSSH or PEM (RSA · EC) private key (ed25519 · rsa · ecdsa) (.pub is the public key)",
"개인 키만 쓰면 비웁니다": "Leave empty if you use only a private key",
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "The private key is protected by a passphrase. Enter the key passphrase",
@ -1340,6 +1343,7 @@
"받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)": "{0} files on the receiving side will be overwritten (same name, different content - the original is lost)",
"받는 쪽이 더 새것 {0} (건너뜀)": "Newer on receiving side {0} (skipped)",
"받는 쪽이 더 새것 {0}개 건너뜀": "{0} skipped (newer on the receiving side)",
"받는 쪽이 더 새것이라 {0}개를 건너뛰었습니다.": "Skipped {0} because the receiving side is newer.",
"받는 쪽이 더 새것이면 건너뛰기": "Skip when the receiving side is newer",
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "Download size: about {0}MB · downloaded files · AI models · settings are kept",
"받다 만 AI 모델": "Unfinished AI models",
@ -2157,6 +2161,7 @@
"이미 실행 중인 창이 응답하지 않습니다.\n\n[예] 그래도 열기 - 같은 데이터 위에 창이 두 개가 되어 설정 · 목록이 서로 덮어쓸 수 있습니다.\n[아니요] 닫기 - 응답하지 않는 창을 작업 관리자에서 끝낸 뒤 다시 실행하세요.": "A window that is already running is not responding.\n\n[Yes] Open anyway - two windows will share the same data, and settings or lists may overwrite each other.\n[No] Close - end the unresponsive window in Task Manager, then run the app again.",
"이미 이 폴더에 있습니다": "Already in this folder",
"이미 이 폴더에 있습니다: {0}": "Already in this folder: {0}",
"이미 이 폴더에 있어 건너뛰었습니다: {0}.": "Skipped, already in this folder: {0}.",
"이미 이 폴더에 있어 건너뜀: {0}": "Skipped, already in this folder: {0}",
"이미 있는 폴더입니다: {0}": "The folder already exists: {0}",
"이미 있어 건너뜀": "Skipped - already exists",

View File

@ -583,6 +583,7 @@
"{0}개 항목을 휴지통으로 보낼까요?": "{0} 個の項目をごみ箱に移動しますか?",
"{0}개 항목을 휴지통으로 보냈습니다.": "{0} 個の項目をごみ箱に移動しました。",
"{0}개는 더 새 파일이라 그대로 둠": "{0} 件は新しいファイルなのでそのまま",
"{0}개는 원본에 남았습니다 (건너뜀).": "{0} 件は元の場所に残りました (スキップ)。",
"{0}개는 원본에 남음 (건너뜀)": "{0} 件は元の場所に残っています (スキップ)",
"{0}개는 지우지 않았습니다 (취소).": "{0} 個は削除しませんでした (キャンセル)。",
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 個はごみ箱より大きいため完全に削除しました (Windows の確認どおり)。",
@ -673,6 +674,7 @@
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "同じ名前を上書きしたファイル {0} 件は元に戻しません。",
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "同じ名前のファイルがあるためフォルダーを作成できません: {0}",
"같은 이름의 폴더가 있어 {0}개를 건너뛰었습니다: {1}.": "同名のフォルダーがあるため {0} 件をスキップしました: {1}。",
"같은 이름의 폴더가 있어 건너뜀 {0}": "同名のフォルダーがあるためスキップ {0}",
"같은 이름의 폴더가 있어 건너뜀 {0}개: {1}": "同名のフォルダーがあるため {0} 件スキップ: {1}",
"같은 이름이 이미 있습니다": "同じ名前のものが既にあります",
@ -681,6 +683,7 @@
"같은 이름이 있는지 확인하는 중 …": "同じ名前があるか確認中 …",
"같은 이름이 있으면": "同じ名前がある場合",
"같은 이름이 있을 때": "同じ名前があるとき",
"같은 이름이라 건너뛰었습니다: {0}.": "同じ名前のためスキップしました: {0}。",
"같은 이름이라 건너뜀 {0}개": "同じ名前のためスキップ {0} 件",
"같은 이름이라 건너뜀: {0}": "同じ名前のためスキップ: {0}",
"같은 조건으로 다시 (시드 그대로)": "同じ条件でもう一度 (シードはそのまま)",
@ -702,7 +705,7 @@
"개인 키 파일 고르기": "秘密鍵ファイルを選択",
"개인 키 파일이 아닙니다 (너무 큼)": "秘密鍵ファイルではありません (大きすぎます)",
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を登録しました (元のファイルのパスは保存しません)",
"개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を入れました。パスフレーズ付きのキーなので下にキーのパスフレーズを入れてください (元のファイルの場所は保存しません)",
"개인 키를 넣었습니다. 암호가 걸린 키라 위의 [{0}] 칸에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を入れました。パスフレーズ付きのキーなので、上の [{0}] 欄にキーのパスフレーズを入れてください (元のファイルの場所は保存しません)",
"개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)": "秘密鍵を読めません。OpenSSH 形式か PEM (RSA・EC) 形式の秘密鍵 (ed25519・rsa・ecdsa) を選んでください (.pub は公開鍵)",
"개인 키만 쓰면 비웁니다": "秘密鍵だけを使うなら空にします",
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "秘密鍵にパスフレーズがかかっています。キーのパスフレーズを入れてください",
@ -1340,6 +1343,7 @@
"받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)": "受け取る側の {0} 件を上書きします (同じ名前で内容が違うもの - 元のものはなくなります)",
"받는 쪽이 더 새것 {0} (건너뜀)": "受け取る側が新しい {0} (スキップ)",
"받는 쪽이 더 새것 {0}개 건너뜀": "受け取る側が新しい {0} 件をスキップ",
"받는 쪽이 더 새것이라 {0}개를 건너뛰었습니다.": "受け取る側が新しいため {0} 件をスキップしました。",
"받는 쪽이 더 새것이면 건너뛰기": "受け取る側が新しければスキップ",
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "ダウンロードサイズ: 約 {0}MB・ダウンロードしたファイル・AI モデル・設定はそのまま",
"받다 만 AI 모델": "ダウンロード途中の AI モデル",
@ -2157,6 +2161,7 @@
"이미 실행 중인 창이 응답하지 않습니다.\n\n[예] 그래도 열기 - 같은 데이터 위에 창이 두 개가 되어 설정 · 목록이 서로 덮어쓸 수 있습니다.\n[아니요] 닫기 - 응답하지 않는 창을 작업 관리자에서 끝낸 뒤 다시 실행하세요.": "すでに実行中のウィンドウが応答しません。\n\n[はい] それでも開く - 同じデータの上にウィンドウが 2 つになり、設定 · 一覧が互いに上書きされることがあります。\n[いいえ] 閉じる - 応答しないウィンドウをタスク マネージャーで終了してから、もう一度実行してください。",
"이미 이 폴더에 있습니다": "既にこのフォルダーにあります",
"이미 이 폴더에 있습니다: {0}": "すでにこのフォルダーにあります: {0}",
"이미 이 폴더에 있어 건너뛰었습니다: {0}.": "既にこのフォルダーにあるためスキップしました: {0}。",
"이미 이 폴더에 있어 건너뜀: {0}": "既にこのフォルダーにあるためスキップ: {0}",
"이미 있는 폴더입니다: {0}": "既にあるフォルダーです: {0}",
"이미 있어 건너뜀": "すでにあるためスキップ",

View File

@ -583,6 +583,7 @@
"{0}개 항목을 휴지통으로 보낼까요?": "将 {0} 个项目移到回收站吗?",
"{0}개 항목을 휴지통으로 보냈습니다.": "已将 {0} 个项目移到回收站。",
"{0}개는 더 새 파일이라 그대로 둠": "{0} 个因现有文件更新而保留",
"{0}개는 원본에 남았습니다 (건너뜀).": "{0} 个留在源中(已跳过)。",
"{0}개는 원본에 남음 (건너뜀)": "{0} 个留在源中(已跳过)",
"{0}개는 지우지 않았습니다 (취소).": "{0} 个未删除 (已取消)。",
"{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 个超出回收站容量,已永久删除 (按 Windows 的提示)。",
@ -673,6 +674,7 @@
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} 个覆盖了同名文件的文件不会撤销。",
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "存在同名文件,无法创建文件夹: {0}",
"같은 이름의 폴더가 있어 {0}개를 건너뛰었습니다: {1}.": "因存在同名文件夹,已跳过 {0} 项: {1}。",
"같은 이름의 폴더가 있어 건너뜀 {0}": "因存在同名文件夹而跳过 {0}",
"같은 이름의 폴더가 있어 건너뜀 {0}개: {1}": "因存在同名文件夹跳过 {0} 项: {1}",
"같은 이름이 이미 있습니다": "已存在同名项目",
@ -681,6 +683,7 @@
"같은 이름이 있는지 확인하는 중 …": "正在检查是否有同名项目 …",
"같은 이름이 있으면": "如果同名",
"같은 이름이 있을 때": "存在同名项目时",
"같은 이름이라 건너뛰었습니다: {0}.": "因同名已跳过: {0}。",
"같은 이름이라 건너뜀 {0}개": "因同名跳过 {0} 个",
"같은 이름이라 건너뜀: {0}": "因同名已跳过: {0}",
"같은 조건으로 다시 (시드 그대로)": "以相同条件再来一次 (种子不变)",
@ -702,7 +705,7 @@
"개인 키 파일 고르기": "选择私钥文件",
"개인 키 파일이 아닙니다 (너무 큼)": "这不是私钥文件 (文件过大)",
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥 (不保存原文件路径)",
"개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥。此密钥设有密码,请在下方输入私钥密码 (不保存原文件路径)",
"개인 키를 넣었습니다. 암호가 걸린 키라 위의 [{0}] 칸에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥。此密钥设有密码,请在上方的 [{0}] 栏输入私钥密码 (不保存原文件路径)",
"개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)": "无法读取私钥。请选择 OpenSSH 或 PEM (RSA · EC) 格式的私钥 (ed25519 · rsa · ecdsa) (.pub 是公钥)",
"개인 키만 쓰면 비웁니다": "仅使用私钥时留空",
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "私钥设有密码。请输入私钥密码",
@ -1340,6 +1343,7 @@
"받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)": "将覆盖接收端的 {0} 个文件(同名但内容不同 - 原文件将丢失)",
"받는 쪽이 더 새것 {0} (건너뜀)": "接收端较新 {0}(跳过)",
"받는 쪽이 더 새것 {0}개 건너뜀": "已跳过接收端较新的 {0} 个",
"받는 쪽이 더 새것이라 {0}개를 건너뛰었습니다.": "因接收端较新,已跳过 {0} 个。",
"받는 쪽이 더 새것이면 건너뛰기": "接收端较新时跳过",
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "下载大小: 约 {0}MB · 已下载的文件 · AI 模型 · 设置均保留",
"받다 만 AI 모델": "未下载完的 AI 模型",
@ -2157,6 +2161,7 @@
"이미 실행 중인 창이 응답하지 않습니다.\n\n[예] 그래도 열기 - 같은 데이터 위에 창이 두 개가 되어 설정 · 목록이 서로 덮어쓸 수 있습니다.\n[아니요] 닫기 - 응답하지 않는 창을 작업 관리자에서 끝낸 뒤 다시 실행하세요.": "已在运行的窗口没有响应。\n\n[是] 仍然打开 - 同一数据上将有两个窗口,设置和列表可能互相覆盖。\n[否] 关闭 - 请在任务管理器中结束无响应的窗口后重新运行。",
"이미 이 폴더에 있습니다": "已在此文件夹中",
"이미 이 폴더에 있습니다: {0}": "已在此文件夹中: {0}",
"이미 이 폴더에 있어 건너뛰었습니다: {0}.": "已在此文件夹中,已跳过: {0}。",
"이미 이 폴더에 있어 건너뜀: {0}": "已在此文件夹中,已跳过: {0}",
"이미 있는 폴더입니다: {0}": "文件夹已存在: {0}",
"이미 있어 건너뜀": "已存在,已跳过",

View File

@ -0,0 +1,104 @@
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/download_manager.dart';
import 'package:jj_mkvmaker/app/i18n_controller.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/core/download_detect.dart';
import 'package:jj_mkvmaker/l10n/tr.dart';
import 'package:jj_mkvmaker/services/downloader.dart';
import 'package:jj_mkvmaker/ui/downloads_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:path/path.dart' as p;
import 'support/temp_dir.dart';
/// 받는 척만 하는 백엔드 (네트워크 없음)
class _Backend implements DownloadBackend {
@override
final DownloadKind kind;
_Backend(this.kind);
@override
Future<(String, List<PlaylistEntry>)?> expandPlaylist(String url) async => null;
@override
Future<void> start(DownloadTask t, void Function() changed) async {
t
..state = DownloadState.downloading
..progress = 0.4
..speed = '1.2MB/s'
..title = kind == DownloadKind.video ? 'Test video' : 'ubuntu-24.04.1-desktop-amd64.iso';
changed();
}
@override
Future<void> pause(DownloadTask t) async => t.state = DownloadState.paused;
@override
Future<void> cancel(DownloadTask t) async => t.state = DownloadState.cancelled;
@override
Future<void> shutdown() async {}
}
/// 185 (관리자 지시 10/21): [다운로드 취소] 를 누르면 묻는 창 - [계속 받기] 면 그대로 받는다. ko · en · ja × 412 · 960
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
for (final lang in ['ko', 'en', 'ja']) {
for (final (w, h) in [(412, 900), (960, 600)]) {
testWidgets('185 계속 받기 $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 = testTempDir('jj_dlcancel_');
final d = DownloadManager(
backends: [_Backend(DownloadKind.video), _Backend(DownloadKind.torrent)],
settings: () => AppSettings()..downloadRoot = tmp.path,
readClipboard: () async => null,
);
final shotKey = GlobalKey();
await t.pumpWidget(RepaintBoundary(
key: shotKey,
child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: DownloadsPage(d: d)),
));
Future<void> settle([int n = 8]) async {
for (var i = 0; i < n; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 60)));
await t.pump();
}
}
Future<void> shot(String name) async {
await settle(3);
expect(t.takeException(), isNull, reason: '$name 넘침');
if (out.isEmpty) return;
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, 'dlcancel_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
}
d.add('https://youtu.be/AAA111');
d.add('magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567');
await settle();
await t.tap(find.text(tr('전체 선택')));
await settle();
await t.tap(find.text(tr('다운로드 취소')));
await settle();
expect(find.text(trf('선택한 다운로드 {0}개를 취소할까요? 잠깐 멈추려면 [일시정지] 를 쓰세요.', [2])), findsOneWidget);
await shot('1_ask');
await t.tap(find.text(tr('계속 받기')));
await settle();
expect(d.tasks.every((x) => x.state == DownloadState.downloading), isTrue, reason: '[계속 받기] 면 그대로');
await shot('2_kept');
expect(p.basename(tmp.path), startsWith('jj_dlcancel_'));
});
}
}
}

View File

@ -84,7 +84,7 @@ void main() {
await settle(() => false, rounds: 10); // 알림이 다 올라오게
expect(File(p.join(right, 'D', 'add.txt')).existsSync(), isTrue);
expect(File(p.join(d, 'same.txt')).readAsStringSync(), 'old', reason: '받는 쪽이 더 새것이라 원본에 남음');
expect(find.textContaining(trf('{0}개는 원본에 남음 (건너뜀)', [1])), findsOneWidget, reason: '264: 알림에');
expect(find.textContaining(trf('{0}개는 원본에 남았습니다 (건너뜀).', [1])), findsOneWidget, reason: '264: 알림에');
expect(find.text('add.txt'), findsNothing, reason: '265: 원본 D 를 다시 읽음');
expect(find.text('same.txt'), findsOneWidget);
await shot('2_after');

View File

@ -117,7 +117,7 @@ void main() {
await shot('1_ppk', 'Export OpenSSH key');
// ② 암호가 걸린 키를 고르면 "아래에 키 암호를"
await pick(keyA);
await shot('2_picked', tr('개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)'));
await shot('2_picked', trf('개인 키를 넣었습니다. 암호가 걸린 키라 위의 [{0}] 칸에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)', [tr('개인 키 암호')]));
// ③ 키 암호 없이 [연결 확인]
await testConnection();
await shot('3_needpass', tr('개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요'));

View File

@ -3317,14 +3317,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
? trf('끝나지 못했습니다: {0}', [error])
: [
trf('{0}개 항목을 {1}', [job.made.length, move ? tr('옮겼습니다.') : tr('복사했습니다.')]),
// 관리자 지시 (10/21, 15 찾음 F3): 뒤 조각도 문장으로 쓰고 빈칸으로 잇는다 (예전에는 "…옮겼습니다. · 1개는 …" 처럼 마침표 뒤에 " · ")
// 264: 작업 기록에만 있던 것도 알림에 (폴더를 합칠 때 받는 쪽이 더 새 파일 등)
if (move && job.leftInSource > 0) trf('{0}개는 원본에 남음 (건너뜀)', [job.leftInSource]),
if (!move && job.keptNewer > 0) trf('받는 쪽이 더 새것 {0}개 건너뜀', [job.keptNewer]),
if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]),
if (move && job.leftInSource > 0) trf('{0}개는 원본에 남았습니다 (건너뜀).', [job.leftInSource]),
if (!move && job.keptNewer > 0) trf('받는 쪽이 더 새것이라 {0}개를 건너뛰었습니다.', [job.keptNewer]),
if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뛰었습니다: {0}.', [_namesOf(job.skipped)]),
// M1 (10/19): 원격 맞추기에서 받는 쪽의 같은 이름 폴더는 --delete 없이 지우지 않는다
if (job.dirConflicts.isNotEmpty) trf('같은 이름의 폴더가 있어 건너뜀 {0}개: {1}', [job.dirConflicts.length, _namesOf(job.dirConflicts)]),
if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]),
].join(' · ');
if (job.dirConflicts.isNotEmpty) trf('같은 이름의 폴더가 있어 {0}개를 건너뛰었습니다: {1}.', [job.dirConflicts.length, _namesOf(job.dirConflicts)]),
if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뛰었습니다: {0}.', [_namesOf(skippedHere)]),
].join(' ');
// 85: 끝나면 [폴더 열기] - 앱 탐색기에서 넣은 폴더로 (그 폴더를 보고 있는 창, 없으면 지금 창)
if (error == null && mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(

View File

@ -231,7 +231,10 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
final encrypted = checkPrivateKey(text);
set(() {
key = text;
result = encrypted ? tr('개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)') : tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)');
// 관리자 지시 (10/21, 15 찾음 F4): 안내는 창 맨 아래 - 키 암호 칸은 그 위 (칸 이름은 실제 칸 이름 그대로)
result = encrypted
? trf('개인 키를 넣었습니다. 암호가 걸린 키라 위의 [{0}] 칸에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)', [tr('개인 키 암호')])
: tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)');
passed = true;
});
} catch (e) {

View File

@ -134,7 +134,7 @@ void main() {
await settle(tester, () => find.byType(LinearProgressIndicator).evaluate().isEmpty && find.byType(SnackBar).evaluate().isNotEmpty);
expect(File(p.join(right, 'D', 'add.txt')).existsSync(), isTrue);
expect(File(p.join(d, 'same.txt')).existsSync(), isTrue, reason: '받는 쪽이 더 새것이라 원본에 남음');
expect(find.textContaining('1개는 원본에 남음 (건너뜀)'), findsOneWidget, reason: '264: 알림에도');
expect(find.textContaining('1개는 원본에 남았습니다 (건너뜀).'), findsOneWidget, reason: '264: 알림에도');
expect(find.text('add.txt'), findsNothing, reason: '265: 원본 D 를 다시 읽음 (오른쪽 D 는 펼치지 않음)');
expect(find.text('same.txt'), findsOneWidget);
await calm(tester);
@ -359,13 +359,13 @@ void main() {
expect(find.textContaining('2개 표시함'), findsOneWidget);
await tester.runAsync(() => tester.tap(find.text('이동').first));
await settle(tester, () => find.byType(AlertDialog).evaluate().isNotEmpty, rounds: 30);
expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget);
expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget, reason: '확인 창 (설명 목록 꼴 그대로)');
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '이동')));
await settle(tester, () => File(p.join(right, 'a.txt')).existsSync() && find.byType(SnackBar).evaluate().isNotEmpty);
expect(File(p.join(right, 'a.txt')).readAsStringSync(), 'a');
expect(File(p.join(left, 'a.txt')).existsSync(), isFalse);
expect(File(p.join(right, 'b.txt')).readAsStringSync(), 'b');
expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget, reason: '끝난 알림에도');
expect(find.textContaining('이미 이 폴더에 있어 건너뛰었습니다: b.txt.'), findsOneWidget, reason: '끝난 알림에도');
});
testWidgets('147: 버튼 줄이 다 들어가지 않으면 (낮은 화면) 나머지는 [더 보기] 메뉴로 - "숨은 항목 표시" 도 누를 수 있다', (tester) async {
@ -522,6 +522,6 @@ void main() {
await confirm(tester);
expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'old');
expect(File(p.join(right, 'doc (2).txt')).existsSync(), isFalse);
expect(find.textContaining('같은 이름이라 건너뜀: doc.txt'), findsOneWidget);
expect(find.textContaining('같은 이름이라 건너뛰었습니다: doc.txt.'), findsOneWidget);
});
}