diff --git a/assets/l10n/en.json b/assets/l10n/en.json index a7bbe72..c271ad2 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -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", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 36d5fe9..c1bf554 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -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}", "이미 있어 건너뜀": "すでにあるためスキップ", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 6c132c9..ddffda0 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -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}", "이미 있어 건너뜀": "已存在,已跳过", diff --git a/integration_test/windows_dlcancel_shot_test.dart b/integration_test/windows_dlcancel_shot_test.dart new file mode 100644 index 0000000..bfaf2fc --- /dev/null +++ b/integration_test/windows_dlcancel_shot_test.dart @@ -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)?> expandPlaylist(String url) async => null; + @override + Future 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 pause(DownloadTask t) async => t.state = DownloadState.paused; + @override + Future cancel(DownloadTask t) async => t.state = DownloadState.cancelled; + @override + Future 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 settle([int n = 8]) async { + for (var i = 0; i < n; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 60))); + await t.pump(); + } + } + + Future shot(String name) async { + await settle(3); + 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_')); + }); + } + } +} diff --git a/integration_test/windows_move_left_shot_test.dart b/integration_test/windows_move_left_shot_test.dart index 766a463..75ebf01 100644 --- a/integration_test/windows_move_left_shot_test.dart +++ b/integration_test/windows_move_left_shot_test.dart @@ -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'); diff --git a/integration_test/windows_sshkey_shot_test.dart b/integration_test/windows_sshkey_shot_test.dart index 72cf803..019ec32 100644 --- a/integration_test/windows_sshkey_shot_test.dart +++ b/integration_test/windows_sshkey_shot_test.dart @@ -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('개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요')); diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 1376e42..dbcea2a 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -3317,14 +3317,15 @@ class _ExplorerPageState extends State { ? 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( diff --git a/lib/ui/sftp_settings.dart b/lib/ui/sftp_settings.dart index a7084bd..97bd53b 100644 --- a/lib/ui/sftp_settings.dart +++ b/lib/ui/sftp_settings.dart @@ -231,7 +231,10 @@ Future 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) { diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index f70edd7..5963214 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -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); }); }