WIP X-plore j: [SMB 서버 추가] 창에 [같은 망에서 찾기] (검토 대기)
관리자 지시 (10/18): cd 의 lan_scan.dart (xplorehint_fixb6e4717) 를 묶음7_88 (88 SMB) 위로 가져와 SMB 추가 창의 [찾기] 에 붙인다. - lib/core/lan_scan.dart · test/lan_scan_test.dart:b6e4717의 것 그대로 (cd 작성 - 사설 /24 의 445 포트에 연결만 해 보고 닫음, 64개 동시, Tailscale 은 훑지 않음) - SMB 추가 · 고치기 창: 서버 주소 아래 [같은 망에서 찾기] → 찾는 창 (무엇을 하는지 안내 · 진행 "n / 254 확인" · [취소]). 찾은 서버를 누르면 서버 주소 칸에 이름 (역방향 조회, 모르면 주소), 이름 칸이 비었으면 이름 앞부분을 채움. 못 찾으면 직접 넣으라는 안내 - 누를 때만 훑는다 (저절로 훑지 않음 - 내 판단: 망 전체에 연결을 시도하므로 사용자가 고를 때만) - mDNS (_smb._tcp) 는 아직 (관리자 "더하면 좋음") - 포트 훑기로 찾은 것의 이름은 역방향 DNS 시험: smb_find_test (가짜 찾기 - 실제 망을 훑지 않음: 고르면 주소 · 이름 채움, 이름 모르면 주소, 못 찾음 안내, 추가 창 유지), lan_scan_test (127.0.0.1 만), smb_test - 7 통과, analyze 문제 없음, l10n 빠짐 0. Android 실제 망에서는 기기 확인 필요. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent
e5a115d450
commit
3a1a6391ca
|
|
@ -450,6 +450,7 @@
|
||||||
"{0} (기본): {1}": "{0} (default): {1}",
|
"{0} (기본): {1}": "{0} (default): {1}",
|
||||||
"{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} 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}",
|
||||||
|
|
@ -603,6 +604,8 @@
|
||||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "Downloaded only if the detected language differs from the subtitle language {0}",
|
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "Downloaded only if the detected language differs from the subtitle language {0}",
|
||||||
"강하게 (0.4)": "Strong (0.4)",
|
"강하게 (0.4)": "Strong (0.4)",
|
||||||
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "Stronger refines more but may drift from the original",
|
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "Stronger refines more but may drift from the original",
|
||||||
|
"같은 망에서 찾기": "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}",
|
||||||
|
|
@ -1835,6 +1838,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)",
|
||||||
|
|
@ -2283,6 +2287,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",
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,7 @@
|
||||||
"{0} (기본): {1}": "{0}(既定): {1}",
|
"{0} (기본): {1}": "{0}(既定): {1}",
|
||||||
"{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}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
|
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
|
||||||
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
|
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
|
||||||
|
|
@ -603,6 +604,8 @@
|
||||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "検出した言語が作成する言語と異なる場合のみダウンロード {0}",
|
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "検出した言語が作成する言語と異なる場合のみダウンロード {0}",
|
||||||
"강하게 (0.4)": "強め (0.4)",
|
"강하게 (0.4)": "強め (0.4)",
|
||||||
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "強いほど大きく整えますが、元と違ってくることがあります",
|
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "強いほど大きく整えますが、元と違ってくることがあります",
|
||||||
|
"같은 망에서 찾기": "同じネットワークで探す",
|
||||||
|
"같은 망의 SMB 서버": "同じネットワークの SMB サーバー",
|
||||||
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
|
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
|
||||||
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "同じ名前を上書きしたファイル {0} 件は元に戻しません。",
|
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "同じ名前を上書きしたファイル {0} 件は元に戻しません。",
|
||||||
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "同じ名前のファイルがあるためフォルダーを作成できません: {0}",
|
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "同じ名前のファイルがあるためフォルダーを作成できません: {0}",
|
||||||
|
|
@ -1835,6 +1838,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} (前回の所要時間から)",
|
||||||
|
|
@ -2283,6 +2287,7 @@
|
||||||
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。",
|
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "字幕が見つかりませんでした。タイトルを英語の原題に変えるか、言語を追加してみてください。",
|
||||||
"찾을 글": "検索する文字",
|
"찾을 글": "検索する文字",
|
||||||
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "見つかりません (すでに削除または移動されました)",
|
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "見つかりません (すでに削除または移動されました)",
|
||||||
|
"찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.": "見つかりませんでした。NAS の電源が入っていて同じネットワークにあるか確認するか、アドレスを直接入力してください。",
|
||||||
"채도": "彩度",
|
"채도": "彩度",
|
||||||
"채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)",
|
"채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)",
|
||||||
"처리 장치": "処理装置",
|
"처리 장치": "処理装置",
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,7 @@
|
||||||
"{0} (기본): {1}": "{0}(默认): {1}",
|
"{0} (기본): {1}": "{0}(默认): {1}",
|
||||||
"{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}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
|
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
|
||||||
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
|
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
|
||||||
|
|
@ -603,6 +604,8 @@
|
||||||
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "仅当检测到的语言与要生成的语言不同时下载 {0}",
|
"감지한 원어가 만들 언어와 다를 때만 내려받기 {0}": "仅当检测到的语言与要生成的语言不同时下载 {0}",
|
||||||
"강하게 (0.4)": "强 (0.4)",
|
"강하게 (0.4)": "强 (0.4)",
|
||||||
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "越强优化越多,但可能与原图不同",
|
"강할수록 많이 다듬지만 원본과 달라질 수 있습니다": "越强优化越多,但可能与原图不同",
|
||||||
|
"같은 망에서 찾기": "在同一网络中查找",
|
||||||
|
"같은 망의 SMB 서버": "同一网络中的 SMB 服务器",
|
||||||
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
|
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
|
||||||
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} 个覆盖了同名文件的文件不会撤销。",
|
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} 个覆盖了同名文件的文件不会撤销。",
|
||||||
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "存在同名文件,无法创建文件夹: {0}",
|
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "存在同名文件,无法创建文件夹: {0}",
|
||||||
|
|
@ -1835,6 +1838,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} (按上次耗时)",
|
||||||
|
|
@ -2283,6 +2287,7 @@
|
||||||
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。",
|
"찾은 자막이 없습니다. 제목을 영어 원제로 바꾸거나 언어를 추가해 보세요.": "未找到字幕。请尝试改用英文原名或添加语言。",
|
||||||
"찾을 글": "查找",
|
"찾을 글": "查找",
|
||||||
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "找不到 (已被删除或移动)",
|
"찾을 수 없습니다 (이미 지워졌거나 옮겨졌습니다)": "找不到 (已被删除或移动)",
|
||||||
|
"찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.": "未找到。请确认 NAS 已开启且在同一网络中,或直接输入地址。",
|
||||||
"채도": "饱和度",
|
"채도": "饱和度",
|
||||||
"채우기 (가운데 잘라냄)": "填充(裁切中间)",
|
"채우기 (가운데 잘라냄)": "填充(裁切中间)",
|
||||||
"처리 장치": "处理器",
|
"처리 장치": "处理器",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// X-plore 비교 j: 같은 망에서 SMB (445) 서버 찾기 - 이 기기의 IPv4 망 (/24) 을 짧은 연결 시도로 훑는다.
|
||||||
|
/// 연결만 해 보고 바로 닫는다 (아무것도 보내지 않음). 찾은 것은 이름 (역방향 조회, 모르면 null) 과 함께.
|
||||||
|
/// SMB 서버 추가 창 (88 Android SMB) 의 [찾기] 에서 쓴다.
|
||||||
|
class LanHost {
|
||||||
|
final String address;
|
||||||
|
final String? name;
|
||||||
|
const LanHost(this.address, this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이 기기의 사설 IPv4 주소들 (Wi-Fi · 유선 · Tailscale 은 빼고 - 100.64/10 은 넓어 훑지 않는다)
|
||||||
|
Future<List<InternetAddress>> localPrivateIPv4() async {
|
||||||
|
final out = <InternetAddress>[];
|
||||||
|
for (final ni in await NetworkInterface.list(type: InternetAddressType.IPv4)) {
|
||||||
|
for (final a in ni.addresses) {
|
||||||
|
final b = a.rawAddress;
|
||||||
|
final private = b[0] == 10 || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) || (b[0] == 192 && b[1] == 168);
|
||||||
|
if (private && !a.isLoopback) out.add(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [hosts] 의 [port] 가 열려 있는지 동시에 [parallel] 개씩 본다. 열린 것만 (주소 순서대로)
|
||||||
|
Future<List<LanHost>> scanPort(
|
||||||
|
List<String> hosts, {
|
||||||
|
int port = 445,
|
||||||
|
Duration timeout = const Duration(milliseconds: 400),
|
||||||
|
int parallel = 64,
|
||||||
|
bool Function()? cancelled,
|
||||||
|
void Function(int done, int total)? onProgress,
|
||||||
|
bool lookupNames = true,
|
||||||
|
}) async {
|
||||||
|
final open = <String>{};
|
||||||
|
var next = 0, done = 0;
|
||||||
|
Future<void> worker() async {
|
||||||
|
while (next < hosts.length) {
|
||||||
|
if (cancelled?.call() ?? false) return;
|
||||||
|
final h = hosts[next++];
|
||||||
|
try {
|
||||||
|
final s = await Socket.connect(h, port, timeout: timeout);
|
||||||
|
open.add(h);
|
||||||
|
s.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
onProgress?.call(++done, hosts.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait([for (var i = 0; i < parallel; i++) worker()]);
|
||||||
|
final sorted = hosts.where(open.contains).toList();
|
||||||
|
final out = <LanHost>[];
|
||||||
|
for (final h in sorted) {
|
||||||
|
String? name;
|
||||||
|
if (lookupNames) {
|
||||||
|
try {
|
||||||
|
final r = await InternetAddress(h).reverse().timeout(const Duration(seconds: 1));
|
||||||
|
if (r.host != h) name = r.host;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
out.add(LanHost(h, name));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이 기기의 사설망 (/24 마다) 의 SMB 서버들
|
||||||
|
Future<List<LanHost>> findSmbServers({bool Function()? cancelled, void Function(int done, int total)? onProgress}) async {
|
||||||
|
final hosts = <String>[];
|
||||||
|
for (final a in await localPrivateIPv4()) {
|
||||||
|
final b = a.rawAddress;
|
||||||
|
for (var i = 1; i < 255; i++) {
|
||||||
|
final h = '${b[0]}.${b[1]}.${b[2]}.$i';
|
||||||
|
if (!hosts.contains(h)) hosts.add(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scanPort(hosts, cancelled: cancelled, onProgress: onProgress);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/lan_scan.dart';
|
||||||
|
|
||||||
|
/// X-plore 비교 j: 같은 망 SMB 서버 찾기의 포트 훑기 (실제 망 대신 이 기기의 열린 · 닫힌 포트로)
|
||||||
|
void main() {
|
||||||
|
test('열린 포트만 찾는다 · 순서는 넣은 대로 · 진행 · 취소', () async {
|
||||||
|
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
addTearDown(server.close);
|
||||||
|
server.listen((s) => s.destroy());
|
||||||
|
final closed = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
final closedPort = closed.port;
|
||||||
|
await closed.close();
|
||||||
|
var progress = 0;
|
||||||
|
final found = await scanPort(['127.0.0.1', '127.0.0.2'], port: server.port, lookupNames: false, onProgress: (d, _) => progress = d);
|
||||||
|
expect(found.map((h) => h.address), ['127.0.0.1'], reason: '127.0.0.2 는 듣지 않음 (Windows 는 127.0.0.1 에만 묶임)');
|
||||||
|
expect(progress, 2);
|
||||||
|
expect(await scanPort(['127.0.0.1'], port: closedPort, lookupNames: false), isEmpty);
|
||||||
|
expect(await scanPort(['127.0.0.1'], port: server.port, lookupNames: false, cancelled: () => true), isEmpty, reason: '취소');
|
||||||
|
expect((await localPrivateIPv4()).every((a) => !a.isLoopback), isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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: '추가 창은 그대로');
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue