저장 줄 · 복사 창 안내 · Android SMB 안내 (176 · 178 보충, 179)

- [보기] 로 비교 후 저장하면 작업 현황 줄이 "저장했습니다: 경로" + [열기] (저장하지 않으면 "…: 저장하지 않았습니다")
- 복사 창: 설정 옵션의 원본 지우기 옵션 (/MOV · --remove-source-files 등) 은 복사에서 쓰지 않는다고 알림 (이동은 [이동])
- 179: Android 경로 입력에 smb:// · \서버 를 넣으면 "네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요" + [WebDAV 서버 추가]
- 시험: 저장 줄 · 복사 창 안내 · SMB 안내

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
RyuJongJin 2026-10-10 07:30:59 +09:00
parent e6556a086e
commit 5e5182178b
8 changed files with 97 additions and 1 deletions

View File

@ -351,6 +351,7 @@
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: moves this video to {1}",
"{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: not moved because it is being processed",
"{0}: 저장하지 않았습니다": "{0}: not saved",
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: moves the {1} checked videos to {2}",
"{0}개": "{0}",
"{0}개 · {1}": "{0} · {1}",
@ -594,6 +595,7 @@
"넘기는 방향: 왼쪽 → 오른쪽": "Page direction: left → right",
"넣지 못했습니다: {0}": "Could not add: {0}",
"네덜란드어": "Dutch",
"네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요": "Network shares (SMB) aren't supported yet. Use the WebDAV tab",
"네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).": "For a network share, include the shared folder name (e.g. \\\\NAS\\Videos).",
"네팔어": "Nepali",
"노랑": "Yellow",
@ -2034,6 +2036,7 @@
"환경 설정 열기": "Open settings",
"환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)": "As set in Settings, it {0} (Settings > File explorer > When a file with the same name exists)",
"환경 설정을 열려면 마스터 비밀번호를 넣으세요. 앱을 끌 때까지 다시 묻지 않습니다.": "Enter the master password to open settings. It won't be asked again until the app is closed.",
"환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)": "{0} from Settings is not used when copying (to move, use [Move])",
"회전": "Rotation",
"회전 없음": "No rotation",
"회차": "Episode",

View File

@ -351,6 +351,7 @@
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: この動画を {1} に移動します",
"{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: 作業中のため移動しませんでした",
"{0}: 저장하지 않았습니다": "{0}: 保存しませんでした",
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: チェックした動画 {1}個を {2} に移動します",
"{0}개": "{0}個",
"{0}개 · {1}": "{0}個・{1}",
@ -594,6 +595,7 @@
"넘기는 방향: 왼쪽 → 오른쪽": "めくる方向: 左 → 右",
"넣지 못했습니다: {0}": "追加できませんでした: {0}",
"네덜란드어": "オランダ語",
"네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요": "ネットワーク共有 (SMB) にはまだ対応していません。WebDAV タブを使ってください",
"네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).": "ネットワーク共有は共有フォルダー名まで入力してください (例: \\\\NAS\\動画)。",
"네팔어": "ネパール語",
"노랑": "黄色",
@ -2034,6 +2036,7 @@
"환경 설정 열기": "環境設定を開く",
"환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)": "設定どおり{0} (環境設定 > ファイルエクスプローラー > 同じ名前があるとき)",
"환경 설정을 열려면 마스터 비밀번호를 넣으세요. 앱을 끌 때까지 다시 묻지 않습니다.": "環境設定を開くにはマスターパスワードを入力してください。アプリを終了するまで再度尋ねません。",
"환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)": "環境設定の {0} はコピーでは使いません (移動は [移動] で)",
"회전": "回転",
"회전 없음": "回転なし",
"회차": "話数",

View File

@ -351,6 +351,7 @@
"{0}: 이 동영상을 {1} 로 옮깁니다": "{0}: 将此视频移到 {1}",
"{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: 正在处理,未移动",
"{0}: 저장하지 않았습니다": "{0}:未保存",
"{0}: 체크한 동영상 {1}개를 {2} 로 옮깁니다": "{0}: 将勾选的 {1} 个视频移到 {2}",
"{0}개": "{0} 个",
"{0}개 · {1}": "{0} 个 · {1}",
@ -594,6 +595,7 @@
"넘기는 방향: 왼쪽 → 오른쪽": "翻页方向: 左 → 右",
"넣지 못했습니다: {0}": "无法加入: {0}",
"네덜란드어": "荷兰语",
"네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요": "尚不支持网络共享(SMB)。请使用 WebDAV 标签",
"네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).": "网络共享请输入到共享文件夹名称 (例: \\\\NAS\\视频)。",
"네팔어": "尼泊尔语",
"노랑": "黄色",
@ -2034,6 +2036,7 @@
"환경 설정 열기": "打开设置",
"환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)": "按设置{0} (设置 > 文件资源管理器 > 存在同名项目时)",
"환경 설정을 열려면 마스터 비밀번호를 넣으세요. 앱을 끌 때까지 다시 묻지 않습니다.": "要打开设置,请输入主密码。在关闭应用前不会再次询问。",
"환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)": "设置中的 {0} 不用于复制(要移动请用 [移动])",
"회전": "旋转",
"회전 없음": "不旋转",
"회차": "集数",

View File

@ -300,7 +300,13 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
AiJobs.instance.done(text,
view: () async {
AiJobs.instance.clearDoneView();
await _compareAndSave(nav, c, result, input: input, nameFrom: nameFrom, outDir: outDir);
final saved = await _compareAndSave(nav, c, result, input: input, nameFrom: nameFrom, outDir: outDir);
// 작업 현황 줄도 결과로 (어디에 저장했는지 + [열기])
if (saved != null) {
AiJobs.instance.done(trf('저장했습니다: {0}', [saved]), dir: p.dirname(saved));
} else {
AiJobs.instance.done(trf('{0}: 저장하지 않았습니다', [nameShort]));
}
},
dispose: () {
try {

View File

@ -65,6 +65,10 @@ class ExplorerPage extends StatefulWidget {
@visibleForTesting
static List<(String, String)> Function()? debugVolumes;
/// 시험: 경로 입력을 Android 처럼 (SMB 안내, 179). null 이면 실제 기기
@visibleForTesting
static bool? debugAndroidPaths;
static const routeName = 'explorer';
static const rsyncRouteName = 'rsync';
static int _open = 0, _openRsync = 0;
@ -1848,6 +1852,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
'\n\n${trf('방법: {0}', [tr(method.label)])}'
'${method == CopyMethod.builtin ? '' : ' · ${withoutSourceRemovingOptions(task.options)}${move ? (method == CopyMethod.rsync ? ' --remove-source-files' : ' /MOVE') : ''}'}'
'${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'),
// 178 보충: 환경 설정의 원본 지우기 옵션은 복사에서 뺀다 - 왜 빠졌는지 알림
if (!move && method != CopyMethod.builtin && optionsRemoveSource(task.options))
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
trf('환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)',
[splitOptions(task.options).where(isSourceRemovingOption).join(' ')]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
),
FutureBuilder<List<NameClash>>(
future: clashesFuture,
builder: (ctx, snap) {
@ -3083,6 +3096,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
setState(() => _active = _panes.indexOf(pane));
final path = await showDialog<String>(context: context, builder: (_) => _PathDialog(initial: pane.current));
if (path == null || !mounted) return;
// 179: SMB 를 넣었다가 [WebDAV 서버 추가] 를 누름
if (path == _PathDialog.addDav) return _editDav(pane, null);
await _goTo(pane, path, fresh: true);
}
@ -3090,6 +3105,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
static Future<(String?, String?)> checkEnteredPath(String input) async {
var path = input.trim().replaceAll('"', '');
if (path.isEmpty) return (null, null);
// 179: Android 는 네트워크 공유 (SMB) 를 아직 못 연다 - 폴더가 없다고 하지 않고 WebDAV 로 안내
if ((ExplorerPage.debugAndroidPaths ?? Platform.isAndroid) && looksLikeSmb(path)) return (null, smbNotSupported);
if (Platform.isWindows && RegExp(r'^\\\\[^\\/]+[\\/]?$').hasMatch(path)) {
return (null, tr('네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).'));
}
@ -3732,10 +3749,18 @@ class _PatternDialogState extends State<_PatternDialog> {
);
}
/// 179: SMB 경로 (smb:// · \\서버\공유)
bool looksLikeSmb(String path) => RegExp(r'^(smb|cifs)://', caseSensitive: false).hasMatch(path) || path.startsWith(r'\\');
String get smbNotSupported => tr('네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요');
class _PathDialog extends StatefulWidget {
const _PathDialog({required this.initial});
final String initial;
/// 창이 이것을 돌려주면 WebDAV 서버 추가 창을 연다
static const addDav = '\u0000webdav';
@override
State<_PathDialog> createState() => _PathDialogState();
}
@ -3778,6 +3803,9 @@ class _PathDialogState extends State<_PathDialog> {
onSubmitted: (_) => _submit(),
),
actions: [
// 179: SMB 를 넣었으면 바로 WebDAV 서버 추가로
if (_error == smbNotSupported)
TextButton(onPressed: () => Navigator.pop(context, _PathDialog.addDav), child: Text(tr('WebDAV 서버 추가'))),
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
FilledButton(onPressed: _checking ? null : _submit, child: Text(tr('확인'))),
],

View File

@ -406,6 +406,33 @@ void main() {
expect(find.textContaining(right), findsWidgets);
});
testWidgets('179: Android 에서 SMB 경로를 넣으면 "아직 지원하지 않음 · WebDAV" 안내와 [WebDAV 서버 추가]', (tester) async {
ExplorerPage.debugAndroidPaths = true;
addTearDown(() => ExplorerPage.debugAndroidPaths = null);
expect([for (final x in ['smb://nas/v', r'\\nas\v', 'SMB://x', '/storage/x', 'dav://a/']) looksLikeSmb(x)], [true, true, true, false, false]);
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
await tester.tap(find.byTooltip('경로 입력').first);
await tester.pumpAndSettle();
expect(find.widgetWithText(TextButton, 'WebDAV 서버 추가'), findsNothing);
await tester.enterText(find.byType(TextField).last, 'smb://nas/영상');
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '확인')));
await settle(tester, () => find.textContaining('SMB').evaluate().isNotEmpty);
expect(find.text('네트워크 공유 (SMB) 는 아직 지원하지 않습니다. WebDAV 탭을 쓰세요'), findsOneWidget);
expect(find.text('폴더가 없거나 열 수 없습니다'), findsNothing);
// [WebDAV 서버 추가] → 서버 추가 창
await tester.tap(find.widgetWithText(TextButton, 'WebDAV 서버 추가'));
await settle(tester, () => find.text('서버 이름').evaluate().isNotEmpty || find.textContaining('주소').evaluate().isNotEmpty, rounds: 50);
expect(find.widgetWithText(AlertDialog, 'WebDAV 서버 추가'), findsOneWidget);
expect(find.textContaining('SMB'), findsNothing, reason: '경로 창은 닫힘');
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
});
testWidgets('37-2: 보던 폴더가 밖에서 지워지면 새로 읽을 때 남아 있는 상위 폴더로 옮기고 알린다', (tester) async {
final deep = Directory(p.join(left, 'a', 'b'))..createSync(recursive: true);
c.settings.explorerPaths = [deep.path, right];

View File

@ -121,6 +121,29 @@ void main() {
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
});
testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async {
c.settings
..copyMethodFile = 'robocopy'
..robocopyOptions = '/E /MOV';
await open(tester);
await act(tester, () => tester.tap(find.text('doc.txt')));
await settle(tester, () => false, rounds: 15);
await act(tester, () => tester.tap(find.text('복사').first));
await tester.pumpAndSettle();
expect(find.textContaining('방법: robocopy'), findsOneWidget);
expect(find.text('환경 설정의 /MOV 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)'), findsOneWidget);
await act(tester, () => tester.tap(find.text('취소').last));
await tester.pumpAndSettle();
await act(tester, () => tester.tap(find.text('이동').first));
await tester.pumpAndSettle();
expect(find.textContaining('옮길까요?'), findsOneWidget);
expect(find.textContaining('복사에서는 쓰지 않습니다'), findsNothing);
await act(tester, () => tester.tap(find.text('취소').last));
await tester.pumpAndSettle();
expect(File(p.join(left, 'doc.txt')).existsSync(), isTrue);
expect(File(p.join(right, 'doc.txt')).existsSync(), isFalse);
}, skip: !Platform.isWindows);
testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async {
c.settings.explorerClick = 'open';
await open(tester);

View File

@ -115,6 +115,9 @@ void main() {
final saved = [for (final f in tmp.listSync(recursive: true).whereType<File>()) if (!p.equals(f.path, input)) f.path];
expect(saved, isNotEmpty, reason: '저장됨 (원본 옆, 새 파일)');
expect(AiJobs.instance.lastDoneView, isNull, reason: '[보기] 는 한 번');
// 작업 현황 줄도 저장한 곳으로 ([열기] 가 그 폴더)
expect(AiJobs.instance.lastDone, '저장했습니다: ${saved.single}');
expect(AiJobs.instance.lastDoneDir, p.dirname(saved.single));
ScaffoldMessenger.of(tester.element(find.text('올리기'))).clearSnackBars();
await tester.pumpAndSettle();
});