F1 비밀번호 · 키 칸 이름은 짧게, 괄호 설명은 칸 아래 도움 글로 · 131 쿠키 안내 글을 다른 설명 글과 같은 크기 · 색으로 (관리자 지시 10/21, 15 찾음)

- F1: 412 폭 en · ja 에서 👁 자리 때문에 칸 이름이 "…" 로 잘렸다 (SFTP 추가 창 "Password (leave empty if usin…")
  · SFTP 비밀번호 "비밀번호" + 도움 글 "개인 키만 쓰면 비웁니다", 개인 키 암호 "개인 키 암호" + "없으면 비웁니다"
  · AI 인터넷 서비스 "API 키" / "API 키 또는 아이디:비밀번호" + "넣지 않아도 됩니다"
  · ZIP 으로 묶기 "비밀번호" (도움 글 "넣으면 AES-256 … 비우면 보통 ZIP" 이 선택임을 알림)
  · WebDAV · SMB · FTP · 마스터 · OpenSubtitles 는 칸 이름이 이미 "비밀번호" 뿐이라 그대로. 옛 열쇠 4개 지움 (en · ja · zh)
- 131: 쿠키 안내 글 12px 흐린 회색 → ListTile 부제목과 같은 bodyMedium · onSurfaceVariant
- 시험: windows_f1_shot_test (ZIP · AI 서비스 창, ko · en · ja × 412 · 960) 추가. f1 · trunc · cookie 캡처 각 6개 통과,
  72장 release\test\shots_f1 - 직접 봄 (en 412 SFTP · ja 412 AI · en 412 ZIP · ja 960 쿠키): 칸 이름 잘림 없음
- 이 고침은 다음 판 (10.20_001 노트에는 넣지 않음, 관리자 결정)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2
RyuJongJin 2026-10-21 02:52:22 +09:00
parent b011cecfe8
commit 53389baa64
8 changed files with 109 additions and 17 deletions

View File

@ -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",

View File

@ -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}",
"없는 항목입니다": "存在しない項目です",
"없으면 비웁니다": "なければ空にします",
"없음": "なし",
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "なし: 詳細の[移動]を初めて押したときにフォルダーを選んで作成します",
"엔진": "エンジン",

View File

@ -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}",
"없는 항목입니다": "项目不存在",
"없으면 비웁니다": "没有则留空",
"없음": "无",
"없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다": "无: 首次在详细信息中点击[移动]时选择文件夹并创建",
"엔진": "引擎",

View File

@ -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<NavigatorState>();
final shotKey = GlobalKey();
await t.pumpWidget(RepaintBoundary(
key: shotKey,
child: MaterialApp(
theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())),
));
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, 'f1_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
}
Future<void> 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: '창만 열고 닫음 - 아무것도 만들지 않음');
});
}
}
}

View File

@ -518,7 +518,10 @@ Future<void> 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)),
),
),

View File

@ -360,8 +360,10 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
autocorrect: false,
enableSuggestions: false,
decoration: InputDecoration(
labelText: tr('비밀번호 (선택)'),
// 관리자 지시 (10/21, 15 찾음 F1): 칸 이름은 짧게, 괄호 설명은 칸 아래 도움 글로 - 412 폭 en · ja 에서 👁 자리 때문에 이름이 "…" 로 잘렸다 - "선택" 은 아래 도움 글 (비우면 보통 ZIP) 이 알린다
labelText: tr('비밀번호'),
helperText: tr('넣으면 AES-256 으로 암호화합니다. 비우면 보통 ZIP'),
helperMaxLines: 3,
isDense: true,
suffixIcon: IconButton(
tooltip: showPass ? tr('비밀번호 숨기기') : tr('비밀번호 보기'),

View File

@ -1077,7 +1077,8 @@ class _SettingsPageState extends State<SettingsPage> {
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)

View File

@ -274,7 +274,10 @@ Future<SftpServer?> 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<SftpServer?> 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,