import 'dart:io'; import 'package:flutter/material.dart'; import '../../core/vfs.dart' show isDav; import '../../l10n/tr.dart'; /// 약속 (E5d-3): 경로 입력 창 (37 · 146 · 179 · 190). 넣은 경로를 [checkEnteredPath] 로 고쳐 쓰고 확인해, /// 갈 수 있으면 그 경로를 돌려주고 (창 닫힘), 아니면 칸 아래에 이유를 보인다 (창은 그대로 - 고쳐서 다시 넣게). /// [android]: SMB 주소면 열지 않고 [SMB 서버 추가] · [WebDAV 서버 추가] 로 안내 ([PathDialog.addSmb] · [PathDialog.addDav] 를 돌려줌). /// 179: SMB 경로 (smb:// · \\서버\공유) bool looksLikeSmb(String path) => RegExp(r'^(smb|cifs)://', caseSensitive: false).hasMatch(path) || path.startsWith(r'\\'); /// 190: smb://서버/공유/폴더 (cifs:// 도) → \\서버\공유\폴더. SMB 주소가 아니면 null String? smbToUnc(String path) { final m = RegExp(r'^(smb|cifs)://(.+)$', caseSensitive: false).firstMatch(path.trim()); if (m == null) return null; final parts = m.group(2)!.split(RegExp(r'[/\\]+')).where((s) => s.isNotEmpty).map(_decodeSmbPart).toList(); if (parts.isEmpty) return null; return '\\\\${parts.join('\\')}'; } /// %20 같은 주소 글자는 풀고, 한글 등 그대로 쓴 글자는 그대로 String _decodeSmbPart(String s) { try { return Uri.decodeComponent(s); } catch (_) { return s; } } String get smbNotSupported => tr('네트워크 공유 (SMB) 는 경로로 바로 열지 않습니다. [SMB 서버 추가] 로 서버를 넣으면 탭으로 열립니다'); /// 146: 넣은 경로를 고쳐 쓰기 · 확인. 갈 수 있으면 그 경로, 아니면 칸 아래에 보일 이유 Future<(String?, String?)> checkEnteredPath(String input, {required bool android}) async { var path = input.trim().replaceAll('"', ''); if (path.isEmpty) return (null, null); // 179: Android 는 네트워크 공유 (SMB) 를 아직 못 연다 - 폴더가 없다고 하지 않고 WebDAV 로 안내 if (android && looksLikeSmb(path)) return (null, smbNotSupported); // 190: Windows 는 smb:// · cifs:// 주소를 \\서버\공유 로 바꿔 연다 (주소가 틀린 줄 알지 않게) final unc = Platform.isWindows ? smbToUnc(path) : null; if (unc != null) path = unc; if (Platform.isWindows && RegExp(r'^\\\\[^\\/]+[\\/]?$').hasMatch(path)) { return (null, tr('네트워크 공유는 공유 폴더 이름까지 넣으세요 (예: \\\\NAS\\영상).')); } if (!isDav(path) && Platform.isWindows && RegExp(r'^[a-zA-Z]:$').hasMatch(path)) path = '$path\\'; bool ok; try { ok = isDav(path) || await Directory(path).exists(); } catch (_) { ok = false; } if (!ok && unc != null) { return (null, trf('폴더가 없거나 열 수 없습니다. Windows 의 네트워크 공유는 \\\\서버\\공유 형식입니다 (바꾼 경로: {0})', [unc])); } return ok ? (path, null) : (null, tr('폴더가 없거나 열 수 없습니다')); } class PathDialog extends StatefulWidget { const PathDialog({super.key, required this.initial, required this.android}); final String initial; final bool android; /// 창이 이것을 돌려주면 WebDAV 서버 추가 창을 연다 static const addDav = '\u0000webdav'; /// 88: 이것 + 넣은 주소를 돌려주면 SMB 서버 추가 창 (주소로 채움) static const addSmb = '\u0000smb:'; @override State createState() => _PathDialogState(); } class _PathDialogState extends State { late final _ctl = TextEditingController(text: widget.initial) ..selection = TextSelection(baseOffset: 0, extentOffset: widget.initial.length); String? _error; bool _checking = false; @override void dispose() { _ctl.dispose(); super.dispose(); } Future _submit() async { setState(() => _checking = true); final (path, error) = await checkEnteredPath(_ctl.text, android: widget.android); if (!mounted) return; setState(() { _checking = false; _error = error; }); if (path != null) Navigator.pop(context, path); } @override Widget build(BuildContext context) => AlertDialog( scrollable: true, title: Text(Platform.isWindows ? tr('경로 입력 (예: D:\\영상 · \\\\NAS\\공유)') : tr('경로 입력')), content: TextField( controller: _ctl, autofocus: true, decoration: InputDecoration(hintText: tr('경로'), errorText: _error, errorMaxLines: 3), // 고치기 시작하면 앞의 오류는 지운다 onChanged: (_) { if (_error != null) setState(() => _error = null); }, 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, '${PathDialog.addSmb}${_ctl.text}'), child: Text(tr('SMB 서버 추가'))), ], TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))), FilledButton(onPressed: _checking ? null : _submit, child: Text(tr('확인'))), ], ); }