Merge branch 'smb_find' into next

# Conflicts:
#	assets/l10n/en.json
#	assets/l10n/ja.json
#	assets/l10n/zh-Hans.json
hint_fix
RyuJongJin 2026-10-18 01:03:14 +09:00
commit 8d6f288fab
5 changed files with 156 additions and 0 deletions

View File

@ -480,6 +480,7 @@
"{0} (동영상 {1})": "{0} (videos: {1})", "{0} (동영상 {1})": "{0} (videos: {1})",
"{0} - {1} (약 {2}MB)": "{0} - {1} (about {2}MB)", "{0} - {1} (약 {2}MB)": "{0} - {1} (about {2}MB)",
"{0} / {1} 읽음": "{0} / {1} read", "{0} / {1} 읽음": "{0} / {1} read",
"{0} / {1} 확인": "{0} / {1} checked",
"{0} / {1}장": "{0} / {1} images", "{0} / {1}장": "{0} / {1} images",
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)",
"{0} · {1}배 · {2}": "{0} · {1}× · {2}", "{0} · {1}배 · {2}": "{0} · {1}× · {2}",
@ -645,6 +646,8 @@
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "Stronger refines more but may drift from the original", "강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "Stronger refines more but may drift from the original",
"같습니다 ✔": "Match ✔", "같습니다 ✔": "Match ✔",
"같은 네트워크에서만 열 수 있습니다": "Can only be opened from the same network", "같은 네트워크에서만 열 수 있습니다": "Can only be opened from the same network",
"같은 망에서 찾기": "Find on this network",
"같은 망의 SMB 서버": "SMB servers on this network",
"같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)", "같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)",
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} files that replaced a file with the same name won't be undone.", "같은 이름을 덮어쓴 파일 {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}": "Can't create the folder because a file with the same name exists: {0}",
@ -1944,6 +1947,7 @@
"이 기기 (SD1.5 + LCM)": "This device (SD1.5 + LCM)", "이 기기 (SD1.5 + LCM)": "This device (SD1.5 + LCM)",
"이 기기 ({0})": "This device ({0})", "이 기기 ({0})": "This device ({0})",
"이 기기 메모리 {0}GB 기준:": "For this device's {0}GB of memory:", "이 기기 메모리 {0}GB 기준:": "For this device's {0}GB of memory:",
"이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.": "Tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. Nothing is sent.",
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "Only an irreversible hash is stored on this device, not the password itself.", "이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "Only an irreversible hash is stored on this device, not the password itself.",
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)", "이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)",
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)", "이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)",
@ -2420,6 +2424,7 @@
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "No subtitles found. Try the original English title or add languages.", "찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "No subtitles found. Try the original English title or add languages.",
"찾을 글": "Find", "찾을 글": "Find",
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "Not found (already deleted or moved)", "찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "Not found (already deleted or moved)",
"찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.": "Nothing found. Check that the NAS is on and on the same network, or enter the address yourself.",
"채도": "Saturation", "채도": "Saturation",
"채우기 (가운데 잘라냄)": "Fill (crop the center)", "채우기 (가운데 잘라냄)": "Fill (crop the center)",
"처리 장치": "Processor", "처리 장치": "Processor",

View File

@ -480,6 +480,7 @@
"{0} (동영상 {1})": "{0} (動画 {1})", "{0} (동영상 {1})": "{0} (動画 {1})",
"{0} - {1} (약 {2}MB)": "{0} - {1}(約 {2}MB)", "{0} - {1} (약 {2}MB)": "{0} - {1}(約 {2}MB)",
"{0} / {1} 읽음": "{0} / {1} 読み込み済み", "{0} / {1} 읽음": "{0} / {1} 読み込み済み",
"{0} / {1} 확인": "{0} / {1} 確認",
"{0} / {1}장": "{0} / {1} 枚", "{0} / {1}장": "{0} / {1} 枚",
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}", "{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
@ -645,6 +646,8 @@
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "強いほど大きく整えますが、元と違ってくることがあります", "강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "強いほど大きく整えますが、元と違ってくることがあります",
"같습니다 ✔": "一致します ✔", "같습니다 ✔": "一致します ✔",
"같은 네트워크에서만 열 수 있습니다": "同じネットワークからのみ開けます", "같은 네트워크에서만 열 수 있습니다": "同じネットワークからのみ開けます",
"같은 망에서 찾기": "同じネットワークで探す",
"같은 망의 SMB 서버": "同じネットワークの SMB サーバー",
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)", "같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "同じ名前を上書きしたファイル {0} 件は元に戻しません。", "같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "同じ名前を上書きしたファイル {0} 件は元に戻しません。",
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "同じ名前のファイルがあるためフォルダーを作成できません: {0}", "같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "同じ名前のファイルがあるためフォルダーを作成できません: {0}",
@ -1944,6 +1947,7 @@
"이 기기 (SD1.5 + LCM)": "この端末 (SD1.5 + LCM)", "이 기기 (SD1.5 + LCM)": "この端末 (SD1.5 + LCM)",
"이 기기 ({0})": "この端末 ({0})", "이 기기 ({0})": "この端末 ({0})",
"이 기기 메모리 {0}GB 기준:": "この端末のメモリ {0}GB の場合:", "이 기기 메모리 {0}GB 기준:": "この端末のメモリ {0}GB の場合:",
"이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.": "この端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。何も送りません。",
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "この端末にはパスワード自体ではなく、元に戻せないハッシュだけが残ります。", "이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "この端末にはパスワード自体ではなく、元に戻せないハッシュだけが残ります。",
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)", "이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)",
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)", "이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)",
@ -2420,6 +2424,7 @@
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。", "찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。",
"찾을 글": "検索する文字", "찾을 글": "検索する文字",
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "見つかりません (すでに削除または移動されました)", "찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "見つかりません (すでに削除または移動されました)",
"찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.": "見つかりませんでした。NAS の電源が入っていて同じネットワークにあるか確認するか、アドレスを直接入力してください。",
"채도": "彩度", "채도": "彩度",
"채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)", "채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)",
"처리 장치": "処理装置", "처리 장치": "処理装置",

View File

@ -480,6 +480,7 @@
"{0} (동영상 {1})": "{0}(视频 {1})", "{0} (동영상 {1})": "{0}(视频 {1})",
"{0} - {1} (약 {2}MB)": "{0} - {1}(约 {2}MB)", "{0} - {1} (약 {2}MB)": "{0} - {1}(约 {2}MB)",
"{0} / {1} 읽음": "已读取 {0} / {1}", "{0} / {1} 읽음": "已读取 {0} / {1}",
"{0} / {1} 확인": "已检查 {0} / {1}",
"{0} / {1}장": "{0} / {1} 张", "{0} / {1}장": "{0} / {1} 张",
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}", "{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
@ -645,6 +646,8 @@
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "越强优化越多,但可能与原图不同", "강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "越强优化越多,但可能与原图不同",
"같습니다 ✔": "相同 ✔", "같습니다 ✔": "相同 ✔",
"같은 네트워크에서만 열 수 있습니다": "只能在同一网络中打开", "같은 네트워크에서만 열 수 있습니다": "只能在同一网络中打开",
"같은 망에서 찾기": "在同一网络中查找",
"같은 망의 SMB 서버": "同一网络中的 SMB 服务器",
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)", "같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} 个覆盖了同名文件的文件不会撤销。", "같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} 个覆盖了同名文件的文件不会撤销。",
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "存在同名文件,无法创建文件夹: {0}", "같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "存在同名文件,无法创建文件夹: {0}",
@ -1944,6 +1947,7 @@
"이 기기 (SD1.5 + LCM)": "本设备 (SD1.5 + LCM)", "이 기기 (SD1.5 + LCM)": "本设备 (SD1.5 + LCM)",
"이 기기 ({0})": "本设备 ({0})", "이 기기 ({0})": "本设备 ({0})",
"이 기기 메모리 {0}GB 기준:": "按本设备内存 {0}GB:", "이 기기 메모리 {0}GB 기준:": "按本设备内存 {0}GB:",
"이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.": "仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送任何内容。",
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "本设备只保存无法还原的哈希值,而不是密码本身。", "이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "本设备只保存无法还原的哈希值,而不是密码本身。",
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)", "이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)",
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)", "이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)",
@ -2420,6 +2424,7 @@
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。", "찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。",
"찾을 글": "查找", "찾을 글": "查找",
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "找不到 (已被删除或移动)", "찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "找不到 (已被删除或移动)",
"찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.": "未找到。请确认 NAS 已开启且在同一网络中,或直接输入地址。",
"채도": "饱和度", "채도": "饱和度",
"채우기 (가운데 잘라냄)": "填充(裁切中间)", "채우기 (가운데 잘라냄)": "填充(裁切中间)",
"처리 장치": "处理器", "처리 장치": "处理器",

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../app/app_controller.dart'; import '../app/app_controller.dart';
import '../core/lan_scan.dart';
import '../core/secret_gate.dart'; import '../core/secret_gate.dart';
import '../core/smb.dart'; import '../core/smb.dart';
import '../l10n/tr.dart'; import '../l10n/tr.dart';
@ -188,6 +189,22 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
), ),
), ),
]), ]),
// X-plore j: 같은 망 (Wi-Fi · 유선) 에서 SMB 서버 찾기 - 누를 때만 훑는다
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
icon: const Icon(Icons.radar, size: 18),
label: Text(tr('같은 망에서 찾기')),
onPressed: () async {
final h = await pickLanSmbServer(ctx);
if (h == null) return;
set(() {
host.text = h.name ?? h.address;
if (name.text.trim().isEmpty && h.name != null) name.text = h.name!.split('.').first;
});
},
),
),
TextField( TextField(
controller: share, controller: share,
decoration: InputDecoration(labelText: tr('공유 이름'), helperText: tr('예: video · home (\\\\서버\\공유 의 공유)')), decoration: InputDecoration(labelText: tr('공유 이름'), helperText: tr('예: video · home (\\\\서버\\공유 의 공유)')),
@ -247,3 +264,77 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
]); ]);
return saved; return saved;
} }
/// 시험: 찾는 함수 (null 이면 실제로 훑는다)
@visibleForTesting
Future<List<LanHost>> Function({bool Function()? cancelled, void Function(int done, int total)? onProgress})? debugFindSmbServers;
/// X-plore j: 같은 망의 SMB 서버를 찾아 고르는 창 (진행 · 취소). 이 기기의 사설망 (/24) 의 445 포트에 연결만 해 보고 바로 닫는다 (보내는 것 없음)
/// [find]: 시험에서 바꿔 끼움 (실제 망을 훑지 않게)
Future<LanHost?> pickLanSmbServer(BuildContext context) async {
final find = debugFindSmbServers ?? findSmbServers;
var cancelled = false;
var done = 0, total = 0;
List<LanHost>? found;
Object? error;
final picked = await showDialog<LanHost>(
context: context,
builder: (ctx) => StatefulBuilder(builder: (ctx, set) {
if (found == null && error == null && total == 0) {
total = -1;
find(
cancelled: () => cancelled,
onProgress: (d, t) {
if (ctx.mounted) set(() => (done, total) = (d, t));
},
).then((r) {
if (ctx.mounted) set(() => found = r);
}, onError: (Object e) {
if (ctx.mounted) set(() => error = e);
});
}
final list = found;
return AlertDialog(
scrollable: true,
title: Text(tr('같은 망의 SMB 서버')),
content: SizedBox(
width: 460,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr('이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 10),
if (list == null && error == null) ...[
LinearProgressIndicator(value: total > 0 ? done / total : null),
const SizedBox(height: 4),
Text(total > 0 ? trf('{0} / {1} 확인', [done, total]) : tr('준비하는 중'), style: const TextStyle(fontSize: 12)),
],
if (error != null) Text('$error', style: const TextStyle(color: JjColors.danger)),
if (list != null && list.isEmpty) Text(tr('찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.')),
if (list != null)
for (final h in list)
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.dns_outlined),
title: Text(h.name ?? h.address),
subtitle: h.name == null ? null : Text(h.address),
onTap: () => Navigator.pop(ctx, h),
),
]),
),
actions: [
TextButton(
onPressed: () {
cancelled = true;
Navigator.pop(ctx);
},
child: Text(list == null && error == null ? tr('취소') : tr('닫기')),
),
],
);
}),
);
cancelled = true;
return picked;
}

50
test/smb_find_test.dart Normal file
View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/lan_scan.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/smb_settings.dart';
/// X-plore j: [SMB 서버 추가] 창의 [같은 망에서 찾기] - 진행 · 찾은 서버를 고르면 주소 (이름) 칸을 채움. 시험은 실제 망을 훑지 않는다 (가짜 찾기)
void main() {
tearDown(() => debugFindSmbServers = null);
testWidgets('찾기 → 고르면 주소 · 이름 칸 채움, 못 찾으면 안내', (tester) async {
tester.view.physicalSize = const Size(1000, 1200);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
var calls = 0;
var result = const [LanHost('192.168.0.9', 'nas.local'), LanHost('192.168.0.20', null)];
debugFindSmbServers = ({cancelled, onProgress}) async {
calls++;
onProgress?.call(254, 254);
return result;
};
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final key = GlobalKey<NavigatorState>();
await tester.pumpWidget(MaterialApp(navigatorKey: key, home: const Scaffold()));
editSmbServer(key.currentContext!, c);
await tester.pumpAndSettle();
expect(find.text('SMB 서버 추가'), findsOneWidget);
await tester.tap(find.text('같은 망에서 찾기'));
await tester.pumpAndSettle();
expect(calls, 1);
expect(find.text('같은 망의 SMB 서버'), findsOneWidget);
expect(find.text('nas.local'), findsOneWidget);
expect(find.text('192.168.0.20'), findsOneWidget, reason: '이름을 모르면 주소');
await tester.tap(find.text('nas.local'));
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'nas.local'), findsOneWidget, reason: '서버 주소 칸');
expect(find.widgetWithText(TextField, 'nas'), findsOneWidget, reason: '이름 칸 (비어 있었으면)');
// 못 찾음
result = const [];
await tester.tap(find.text('같은 망에서 찾기'));
await tester.pumpAndSettle();
expect(find.textContaining('찾지 못했습니다'), findsOneWidget);
await tester.tap(find.text('닫기'));
await tester.pumpAndSettle();
expect(find.text('SMB 서버 추가'), findsOneWidget, reason: '추가 창은 그대로');
});
}