diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d9e6b66..79fe092 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -137,9 +137,8 @@ "AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때": "When a long task such as AI upscaling finishes", "AI 해상도 올리기: {0}": "AI upscaling: {0}", "API 키": "API key", - "API 키 (선택)": "API key (optional)", "API 키 (필수, 무료)": "API key (required, free)", - "API 키 또는 아이디:비밀번호 (선택)": "API key or user:password (optional)", + "API 키 또는 아이디:비밀번호": "API key or user:password", "AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 video cannot be re-encoded on this device. Change the codec to \"Keep original\" to make the MKV. (The PC version can.)", "Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Since Android 11, no file app can open other apps' data folders (Android/data · Android/obb). Use export or share inside that app to get the files.", "Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView lacks a required feature ({0}). Update Android System WebView / Chrome in the Play Store and open this screen again. Because pages could open without going through Tor, this screen loads nothing.", @@ -697,13 +696,14 @@ "개인 키": "Private key", "개인 키 바꾸기": "Change private key", "개인 키 빼기": "Remove private key", - "개인 키 암호 (없으면 비움)": "Private key passphrase (leave empty if none)", + "개인 키 암호": "Private key passphrase", "개인 키 있음": "Private key set", "개인 키 파일 고르기": "Choose private key file", "개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "This isn't a private key file (choose an OpenSSH or PEM private key, not .pub)", "개인 키 파일이 아닙니다 (너무 큼)": "This isn't a private key file (too large)", "개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "Private key added (the original file path isn't saved)", "개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "Can't read the private key (OpenSSH or PEM format)", + "개인 키만 쓰면 비웁니다": "Leave empty if you use only a private key", "개인 키의 암호가 맞지 않습니다": "The private key passphrase is incorrect", "거꾸로": "Reverse", "거꾸로 된 범위: {0}": "Reversed range: {0}", @@ -924,6 +924,7 @@ "넘기는 방향: 왼쪽 → 오른쪽": "Page direction: left → right", "넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP": "If set, encrypts with AES-256. Leave empty for a normal ZIP", "넣지 못했습니다: {0}": "Could not add: {0}", + "넣지 않아도 됩니다": "Optional", "네덜란드어": "Dutch", "네트워크 · DNS": "Network · DNS", "네트워크 경로 (UNC)": "Network path (UNC)", @@ -1472,7 +1473,6 @@ "비교할 값 붙여넣기 (받은 곳의 해시)": "Paste a value to compare (hash from the source)", "비교할 해시": "Hash to compare", "비밀번호": "Password", - "비밀번호 (개인 키만 쓰면 비움)": "Password (leave empty if using only a private key)", "비밀번호 (선택)": "Password (optional)", "비밀번호 ZIP 을 만들 7-Zip 이 이 판에 없습니다. 비밀번호 없이 묶거나 앱을 다시 설치해 주세요.": "This version has no 7-Zip to make password ZIPs. Make the ZIP without a password or reinstall the app.", "비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "Passwords · API keys (WebDAV · OpenSubtitles · AI services)", @@ -1830,6 +1830,7 @@ "없는 경로: {0}": "path not found: {0}", "없는 경로입니다: {0}": "The path doesn't exist: {0}", "없는 항목입니다": "No such item", + "없으면 비웁니다": "Leave empty if there is none", "없음": "None", "없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "None: one is created when you first press [Move] in the details", "엔진": "Engine", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index bff83cd..2b1da07 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -137,9 +137,8 @@ "AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때": "AI 高解像度化など時間のかかる作業が終わったとき", "AI 해상도 올리기: {0}": "AI 高解像度化: {0}", "API 키": "API キー", - "API 키 (선택)": "API キー (任意)", "API 키 (필수, 무료)": "API キー(必須・無料)", - "API 키 또는 아이디:비밀번호 (선택)": "API キーまたは ID:パスワード (任意)", + "API 키 또는 아이디:비밀번호": "API キーまたは ID:パスワード", "AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)", "Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Android 11 以降、他のアプリのデータフォルダー (Android/data・Android/obb) はどのファイルアプリでも開けません。そのアプリ内のエクスポート・共有でファイルを取り出してください。", "Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView に必要な機能がありません ({0})。Play ストアで Android System WebView・Chrome を更新してから開き直してください。Tor を通らずに開く恐れがあるため、この画面はページを読み込みません。", @@ -697,13 +696,14 @@ "개인 키": "秘密鍵", "개인 키 바꾸기": "秘密鍵を変更", "개인 키 빼기": "秘密鍵を外す", - "개인 키 암호 (없으면 비움)": "秘密鍵のパスフレーズ (なければ空欄)", + "개인 키 암호": "秘密鍵のパスフレーズ", "개인 키 있음": "秘密鍵あり", "개인 키 파일 고르기": "秘密鍵ファイルを選択", "개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "秘密鍵ファイルではありません (OpenSSH・PEM 形式の秘密鍵を選んでください。.pub ではありません)", "개인 키 파일이 아닙니다 (너무 큼)": "秘密鍵ファイルではありません (大きすぎます)", "개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を登録しました (元のファイルのパスは保存しません)", "개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "秘密鍵を読み込めません (OpenSSH・PEM 形式)", + "개인 키만 쓰면 비웁니다": "秘密鍵だけを使うなら空にします", "개인 키의 암호가 맞지 않습니다": "秘密鍵のパスフレーズが正しくありません", "거꾸로": "逆順", "거꾸로 된 범위: {0}": "逆順の範囲: {0}", @@ -924,6 +924,7 @@ "넘기는 방향: 왼쪽 → 오른쪽": "めくる方向: 左 → 右", "넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP": "入力すると AES-256 で暗号化します。空欄なら通常の ZIP", "넣지 못했습니다: {0}": "追加できませんでした: {0}", + "넣지 않아도 됩니다": "入れなくてもかまいません", "네덜란드어": "オランダ語", "네트워크 · DNS": "ネットワーク・DNS", "네트워크 경로 (UNC)": "ネットワークパス (UNC)", @@ -1472,7 +1473,6 @@ "비교할 값 붙여넣기 (받은 곳의 해시)": "比較する値を貼り付け (入手元のハッシュ)", "비교할 해시": "比較するハッシュ", "비밀번호": "パスワード", - "비밀번호 (개인 키만 쓰면 비움)": "パスワード (秘密鍵だけを使う場合は空欄)", "비밀번호 (선택)": "パスワード(任意)", "비밀번호 ZIP 을 만들 7-Zip 이 이 판에 없습니다. 비밀번호 없이 묶거나 앱을 다시 설치해 주세요.": "このバージョンにはパスワード付き ZIP を作る 7-Zip がありません。パスワードなしでまとめるか、アプリを再インストールしてください。", "비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "パスワード・API キー (WebDAV・OpenSubtitles・AI サービス)", @@ -1830,6 +1830,7 @@ "없는 경로: {0}": "存在しないパス: {0}", "없는 경로입니다: {0}": "存在しないパスです: {0}", "없는 항목입니다": "存在しない項目です", + "없으면 비웁니다": "なければ空にします", "없음": "なし", "없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "なし: 詳細の[移動]を初めて押したときにフォルダーを選んで作成します", "엔진": "エンジン", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index c112a03..69b10cb 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -137,9 +137,8 @@ "AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때": "AI 提升分辨率等耗时任务完成时", "AI 해상도 올리기: {0}": "AI 提升分辨率: {0}", "API 키": "API 密钥", - "API 키 (선택)": "API 密钥 (可选)", "API 키 (필수, 무료)": "API 密钥(必填,免费)", - "API 키 또는 아이디:비밀번호 (선택)": "API 密钥或 用户名:密码 (可选)", + "API 키 또는 아이디:비밀번호": "API 密钥或 用户名:密码", "AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)", "Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "从 Android 11 起,任何文件应用都无法打开其他应用的数据文件夹 (Android/data · Android/obb)。请在该应用内使用导出或分享取出文件。", "Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.": "Android System WebView 缺少所需功能 ({0})。请在 Play 商店更新 Android System WebView · Chrome 后重新打开。由于页面可能不经过 Tor 打开,此画面不会加载任何页面。", @@ -697,13 +696,14 @@ "개인 키": "私钥", "개인 키 바꾸기": "更换私钥", "개인 키 빼기": "移除私钥", - "개인 키 암호 (없으면 비움)": "私钥密码 (没有则留空)", + "개인 키 암호": "私钥密码", "개인 키 있음": "已设置私钥", "개인 키 파일 고르기": "选择私钥文件", "개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "这不是私钥文件 (请选择 OpenSSH · PEM 格式的私钥,而不是 .pub)", "개인 키 파일이 아닙니다 (너무 큼)": "这不是私钥文件 (文件过大)", "개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥 (不保存原文件路径)", "개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "无法读取私钥 (OpenSSH · PEM 格式)", + "개인 키만 쓰면 비웁니다": "仅使用私钥时留空", "개인 키의 암호가 맞지 않습니다": "私钥密码不正确", "거꾸로": "倒序", "거꾸로 된 범위: {0}": "范围顺序颠倒: {0}", @@ -924,6 +924,7 @@ "넘기는 방향: 왼쪽 → 오른쪽": "翻页方向: 左 → 右", "넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP": "填写后使用 AES-256 加密。留空则为普通 ZIP", "넣지 못했습니다: {0}": "无法加入: {0}", + "넣지 않아도 됩니다": "可以不填", "네덜란드어": "荷兰语", "네트워크 · DNS": "网络 · DNS", "네트워크 경로 (UNC)": "网络路径 (UNC)", @@ -1472,7 +1473,6 @@ "비교할 값 붙여넣기 (받은 곳의 해시)": "粘贴要比较的值 (来源处的哈希)", "비교할 해시": "要比较的哈希", "비밀번호": "密码", - "비밀번호 (개인 키만 쓰면 비움)": "密码 (仅使用私钥时留空)", "비밀번호 (선택)": "密码(可选)", "비밀번호 ZIP 을 만들 7-Zip 이 이 판에 없습니다. 비밀번호 없이 묶거나 앱을 다시 설치해 주세요.": "此版本没有用于创建加密 ZIP 的 7-Zip。请不设密码压缩,或重新安装应用。", "비밀번호 · API 키 (WebDAV · OpenSubtitles · AI 서비스)": "密码 · API 密钥 (WebDAV · OpenSubtitles · AI 服务)", @@ -1830,6 +1830,7 @@ "없는 경로: {0}": "路径不存在: {0}", "없는 경로입니다: {0}": "路径不存在: {0}", "없는 항목입니다": "项目不存在", + "없으면 비웁니다": "没有则留空", "없음": "无", "없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "无: 首次在详细信息中点击[移动]时选择文件夹并创建", "엔진": "引擎", diff --git a/integration_test/windows_f1_shot_test.dart b/integration_test/windows_f1_shot_test.dart new file mode 100644 index 0000000..2e05c91 --- /dev/null +++ b/integration_test/windows_f1_shot_test.dart @@ -0,0 +1,80 @@ +import 'dart:async'; +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/app_controller.dart'; +import 'package:jj_mkvmaker/app/i18n_controller.dart'; +import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; +import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; +import 'package:jj_mkvmaker/services/platform_services.dart'; +import 'package:jj_mkvmaker/ui/ai_settings.dart'; +import 'package:jj_mkvmaker/ui/archive_ui.dart'; +import 'package:jj_mkvmaker/ui/theme.dart'; +import 'package:path/path.dart' as p; + +import 'support/temp_dir.dart'; + +/// F1 (관리자 지시 10/21, 15 찾음): 비밀번호 · 키 칸 이름은 짧게, 설명은 칸 아래 - ZIP 으로 묶기 창 · AI 인터넷 서비스 추가 창을 +/// ko · en · ja × 412 · 960 으로 찍는다 (SFTP 추가 창은 windows_trunc_shot_test). 아무것도 만들지 않는다 (창만 열고 닫음). +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('F1 칸 이름 $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_f1shot_'); + final file = File(p.join(tmp.path, 'a.txt'))..writeAsStringSync('a'); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final navKey = GlobalKey(); + final shotKey = GlobalKey(); + await t.pumpWidget(RepaintBoundary( + key: shotKey, + child: MaterialApp( + theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())), + )); + 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, 'f1_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + } + + Future closeAll() async { + navKey.currentState!.popUntil((r) => r.isFirst); + await settle(); + } + + unawaited(createZipFlow(navKey.currentContext!, c, [file.path], dir: tmp.path)); + await settle(12); + await shot('zip'); + await closeAll(); + unawaited(editAiService(navKey.currentContext!, c)); + await settle(12); + await shot('ai'); + await closeAll(); + expect(Directory(tmp.path).listSync().map((e) => p.basename(e.path)), ['a.txt'], reason: '창만 열고 닫음 - 아무것도 만들지 않음'); + }); + } + } +} diff --git a/lib/ui/ai_settings.dart b/lib/ui/ai_settings.dart index fd6f78b..5014253 100644 --- a/lib/ui/ai_settings.dart +++ b/lib/ui/ai_settings.dart @@ -518,7 +518,10 @@ Future editAiService(BuildContext context, AppController c, {AiService? ol controller: key, obscureText: hide, decoration: InputDecoration( - labelText: kind == 'a1111' ? tr('API 키 또는 아이디:비밀번호 (선택)') : tr('API 키 (선택)'), + // 관리자 지시 (10/21, 15 찾음 F1): 칸 이름은 짧게, 괄호 설명은 칸 아래 도움 글로 - 412 폭 en · ja 에서 👁 자리 때문에 이름이 "…" 로 잘렸다 + labelText: kind == 'a1111' ? tr('API 키 또는 아이디:비밀번호') : tr('API 키'), + helperText: tr('넣지 않아도 됩니다'), + helperMaxLines: 3, suffixIcon: IconButton(icon: Icon(hide ? Icons.visibility : Icons.visibility_off), onPressed: () => set(() => hide = !hide)), ), ), diff --git a/lib/ui/archive_ui.dart b/lib/ui/archive_ui.dart index 1a14a63..d3a56b8 100644 --- a/lib/ui/archive_ui.dart +++ b/lib/ui/archive_ui.dart @@ -360,8 +360,10 @@ Future createZipFlow(BuildContext context, AppController c, List { Text( tr('로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). ' '이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:'), - style: const TextStyle(fontSize: 12, color: JjColors.textDim)), + // 관리자 지시 (10/21, 131 확인): 12px 흐린 회색은 읽기 힘들었다 - 위아래 설명 글 (ListTile 부제목) 과 같은 크기 · 색으로 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), const SizedBox(height: 6), Wrap(spacing: 6, runSpacing: 6, children: [ for (final d in loginCookieDomains) diff --git a/lib/ui/sftp_settings.dart b/lib/ui/sftp_settings.dart index 7d042f4..ffcb44d 100644 --- a/lib/ui/sftp_settings.dart +++ b/lib/ui/sftp_settings.dart @@ -274,7 +274,10 @@ Future editSftpServer(BuildContext context, AppController c, {SftpS controller: pass, obscureText: hide, decoration: InputDecoration( - labelText: tr('비밀번호 (개인 키만 쓰면 비움)'), + // 관리자 지시 (10/21, 15 찾음 F1): 칸 이름은 짧게, 괄호 설명은 칸 아래 도움 글로 - 412 폭 en · ja 에서 👁 자리 때문에 이름이 "…" 로 잘렸다 + labelText: tr('비밀번호'), + helperText: tr('개인 키만 쓰면 비웁니다'), + helperMaxLines: 3, suffixIcon: IconButton( tooltip: hide ? tr('보이기') : tr('가리기'), icon: Icon(hide ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18), @@ -298,7 +301,7 @@ Future editSftpServer(BuildContext context, AppController c, {SftpS TextField( controller: passphrase, obscureText: hide, - decoration: InputDecoration(labelText: tr('개인 키 암호 (없으면 비움)')), + decoration: InputDecoration(labelText: tr('개인 키 암호'), helperText: tr('없으면 비웁니다'), helperMaxLines: 3), ), TextField( controller: root,