import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import '../core/secret_gate.dart'; import '../core/sftp.dart' show SftpRegistry; import '../core/smb.dart' show SmbRegistry; import '../core/vfs.dart'; import '../core/webdav.dart'; import '../l10n/tr.dart'; import 'android_file_browser.dart'; import 'file_error.dart'; /// 폴더 고르기. Android 는 앱 안 화면 (내장 저장소 · SD 카드 · USB 를 실제 경로로 고를 수 있음), PC 는 Windows 폴더 선택 창 Future pickFolder(BuildContext context, String title, [String? initial]) => Platform.isAndroid ? showAndroidFolderBrowser(context, title: title, initialDirectory: initial) : FilePicker.getDirectoryPath(dialogTitle: title, initialDirectory: initial); /// 124: 이 기기 폴더 또는 WebDAV · SFTP (88) 서버의 폴더 고르기 (서버가 없으면 바로 이 기기 폴더). /// 환경 설정에서 추가한 서버가 실시간 동기화 · 모니터링의 고르는 곳에도 바로 보이게. Future pickFolderOrDav(BuildContext context, String title, [String? initial]) async { // (맨 위 경로, 이름, 설명, SFTP 인지) final servers = [ for (final s in DavRegistry.servers) ('$davScheme${s.id}/', s.label, s.url, false), for (final s in SftpRegistry.servers) ('$sftpScheme${s.id}/', s.label, '${s.user.isEmpty ? '' : '${s.user}@'}${s.host}${s.port == 22 ? '' : ':${s.port}'}', true), for (final s in SmbRegistry.servers) ('$smbScheme${s.id}/', s.label, 'smb://${s.host}/${s.share}', true), ]; if (servers.isEmpty) return pickFolder(context, title, initial); final where = await showDialog( context: context, builder: (ctx) => SimpleDialog( title: Text(title), children: [ SimpleDialogOption( onPressed: () => Navigator.pop(ctx, ''), child: ListTile(leading: const Icon(Icons.folder_outlined), title: Text(tr('이 기기의 폴더'))), ), for (final (root, label, info, sftp) in servers) SimpleDialogOption( onPressed: () => Navigator.pop(ctx, root), child: ListTile( leading: Icon(sftp ? Icons.vpn_key_outlined : Icons.cloud_outlined), title: Text(label), subtitle: Text(info)), ), ], ), ); if (where == null || !context.mounted) return null; if (where.isEmpty) return pickFolder(context, title, initial != null && !isDav(initial) ? initial : null); final start = initial != null && isDav(initial) && vNorm(initial).startsWith(where) ? initial : where; return showDialog(context: context, builder: (_) => _DavFolderDialog(title: title, start: start)); } /// WebDAV 서버 안의 폴더를 들어가며 고른다 class _DavFolderDialog extends StatefulWidget { const _DavFolderDialog({required this.title, required this.start}); final String title; final String start; @override State<_DavFolderDialog> createState() => _DavFolderDialogState(); } class _DavFolderDialogState extends State<_DavFolderDialog> { late String _dir = widget.start; List? _folders; String? _error; @override void initState() { super.initState(); _open(_dir); } Future _open(String dir) async { setState(() { _dir = dir; _folders = null; _error = null; }); try { final list = await vList(dir, strict: true); final names = [for (final e in list) if (e.isDir) e.path]..sort((a, b) => vBasename(a).toLowerCase().compareTo(vBasename(b).toLowerCase())); if (mounted && _dir == dir) setState(() => _folders = names); } catch (e) { if (mounted && _dir == dir) setState(() => _error = '$e'); } } /// 134: 원문 예외 대신 알아볼 수 있는 말 + 할 일. 133: 마스터 때문에 막혔으면 [마스터 비밀번호 넣기] Widget _errorView(String error) { final (title, body) = explainFileError(error, dav: true); final locked = isLockedError(error); return Center( child: Column(mainAxisSize: MainAxisSize.min, children: [ Icon(locked ? Icons.lock_outline : Icons.cloud_off_outlined, color: Colors.redAccent), const SizedBox(height: 8), Text(title, textAlign: TextAlign.center, style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.w600)), if (body.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(body, textAlign: TextAlign.center)), const SizedBox(height: 10), FilledButton.tonal( onPressed: () async { if (locked && !await SecretGate.pass(force: true)) return; await _open(_dir); }, child: Text(locked ? tr('마스터 비밀번호 넣기') : tr('다시 시도')), ), ]), ); } @override Widget build(BuildContext context) { final atRoot = DavPath.parse(_dir).rel.replaceAll('/', '').isEmpty; return AlertDialog( title: Text(widget.title), content: SizedBox( width: 460, height: 380, child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(vDisplay(_dir) + DavPath.parse(_dir).rel, maxLines: 2, overflow: TextOverflow.ellipsis), const Divider(), Expanded( child: _error != null ? _errorView(_error!) : _folders == null ? const Center(child: CircularProgressIndicator()) : ListView(children: [ if (!atRoot) ListTile( dense: true, leading: const Icon(Icons.arrow_upward), title: const Text('..'), onTap: () => _open(vDirname(_dir)), ), for (final f in _folders!) ListTile( dense: true, leading: const Icon(Icons.folder_outlined), title: Text(vBasename(f)), onTap: () => _open(f), ), ]), ), ]), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))), FilledButton(onPressed: _error != null ? null : () => Navigator.pop(context, _dir), child: Text(tr('이 폴더 고르기'))), ], ); } }