# 🧠 راهنمای جامع اتصال و پیاده‌سازی صفحه XMind (Mind-Map) در اپلیکیشن Flutter این مستند فنی برای برنامه‌نویسان فلاتر (Flutter) آماده شده است تا بتوانند صفحه نمایش نمودار درختی و مایند‌مپ احادیث و ارگومان‌ها (`/xmind/`) را با بهترین عملکرد، ظاهر بومی (Native-like) و قابلیت‌های PWA/WebView در اپلیکیشن پیاده‌سازی کنند (مشابه معماری و استانداردی که برای صفحه دستیار هوشمند / Agent پیاده‌سازی شد). --- ## ۱. معماری و چرایی استفاده از WebView/PWA برای این بخش صفحه XMind شامل یک گراف تعاملی سنگین و پویا (پیاده‌سازی‌شده با موتور گرافی ReactFlow) است که قابلیت‌های زیر را ارائه می‌دهد: * رندر دوطرفه نمودار درختی (Root Node دسته‌بندی و شاخه‌های چپ و راست احادیث) * زوم، جابه‌جایی (Pan & Zoom) و فوکوس با انیمیشن روان و ژست‌های لمسی * جستجوی لحظه‌ای روی گره‌ها و هایلایت کردن خودکار * تولید فایل استاندارد `.xmind` بر مبنای ZIP و ساختار XML در لحظه (Client-Side Generation) * کارت اطلاعات تفصیلی ارگومان‌ها با تصاویر نسخ خطی و آدرس‌ها برای حفظ یکپارچگی، سرعت توسعه و امکان به‌روزرسانی زنده نمودارها بدون نیاز به انتشار نسخه جدید اپ، این صفحه از طریق **WebView پیشرفته همراه با JavaScript Bridge دوطرفه (کانال `HabibApp`)** در فلاتر تعبیه می‌شود. --- ## ۲. مشخصات URL و پارامترهای ارسالی آدرس پایه لود صفحه: ```text https://dovodi.newhorizonco.uk/xmind/{category_slug}?token={AUTH_TOKEN}&lang={LANG}&theme={THEME}&platform=flutter ``` ### جدول Query Parameters: | پارامتر | اجباری | توضیحات و مقادیر | | :--- | :---: | :--- | | `category_slug` | بله | اسلاگ دسته‌بندی موضوعی ارگومان‌ها (مثلاً `prophetic-traditions-3`) | | `token` | خیر | توکن احراز هویت Bearer کاربر در صورت لاگین بودن جهت نشان‌گذاری (Bookmark) و دسترسی‌ها | | `lang` | خیر | زبان فعلی اپلیکیشن (`fa`، `ar`، `en`، `ru`) | | `theme` | خیر | تم فعال اپ (`light` یا `dark`) | | `platform` | بله | مقدار ثابت `flutter` جهت مخفی‌سازی هدر و فوتر عمومی سایت و فعال‌سازی پل ارتباطی | --- ## ۳. ارسال تنظیمات اولیه و ابعاد صفحه (`flutterConfig`) به محض پایان بارگذاری صفحه (`onPageFinished`)، فلاتر باید مشخصات Safe Area و ابعاد صفحه را از طریق رویداد سفارشی `flutterConfig` به WebView ارسال کند تا المان‌ها زیر ناچ گوشی یا دکمه‌های ناوبری سیستم قرار نگیرند. ### نمونه کد اجرای جاوااسکریپت در فلاتر: ```dart void sendInitialConfig(WebViewController controller, BuildContext context) { final mediaQuery = MediaQuery.of(context); final isDark = Theme.of(context).brightness == Brightness.dark; final config = { "version": "1.0.0", "platform": { "os": Platform.isAndroid ? "android" : "ios", "isFlutter": true, }, "theme": { "mode": isDark ? "dark" : "light", }, "safeArea": { "top": mediaQuery.padding.top, "bottom": mediaQuery.padding.bottom, "left": mediaQuery.padding.left, "right": mediaQuery.padding.right, }, "screen": { "width": mediaQuery.size.width, "height": mediaQuery.size.height, "orientation": mediaQuery.orientation == Orientation.portrait ? "portrait" : "landscape", } }; final jsonStr = jsonEncode(config); controller.runJavaScript(''' window.dispatchEvent(new CustomEvent('flutterConfig', { detail: $jsonStr })); '''); } ``` --- ## ۴. پل ارتباطی دوطرفه (JavaScript Bridge - `HabibApp`) مشابه صفحه Agent، فلاتر یک کانال جاوااسکریپت با نام `HabibApp` (یا `FlutterChannel`) روی WebView رجیستر می‌کند تا رویدادها و درخواست‌های بومی سمت وب را دریافت نماید: ```dart final channel = JavaScriptChannel( 'HabibApp', onMessageReceived: (JavaScriptMessage message) { handleWebMessage(message.message); }, ); ``` ### ساختار استاندارد پیام‌های ارسالی از وب به فلاتر: ```json { "action": "close_webview | download_file | copy_to_clipboard | open_external_url", "data": { ... } } ``` --- ### جدول اکشن‌های ارسالی از وب به فلاتر: | نام Action | توضیحات | ساختار Data | عملکرد مورد انتظار در فلاتر | | :--- | :--- | :--- | :--- | | `close_webview` | کاربر روی دکمه خروج / بازگشت در نوار ابزار مایند‌مپ کلیک کرد | `{}` | بستن صفحه و بازگشت به صفحه قبل (`Navigator.pop(context)`) | | `download_file` | کاربر دکمه دانلود فایل مایند‌مپ `.xmind` را زد | `{"url": "...", "fileName": "MindMap.xmind", "mimeType": "application/zip", "fileType": "archive"}` | دانلود و ذخیره محلی فایل در پوشه Downloads گوشی | | `copy_to_clipboard` | کپی متن یا آدرس حدیث | `{"text": "...", "label": "...", "showToast": true}` | کپی در کلیپ‌بورد گوشی + نمایش فیدبک لمسی (HapticFeedback) | | `open_external_url` | کلیک روی لینک‌های خارجی منبع | `{"url": "...", "mode": "externalApplication"}` | باز کردن لینک در مرورگر خارجی یا تب جدید | | `share_content` | اشتراک‌گذاری مایند‌مپ یا متن حدیث | `{"title": "...", "text": "...", "url": "..."}` | باز کردن دیالوگ اشتراک‌گذاری بومی سیستم‌عامل (`share_plus`) | --- ## ۵. رویدادهای ارسالی از فلاتر به وب (`flutterEvent`) هر زمان که وضعیت سیستم تغییر کرد (مثل چرخش گوشی یا تغییر تم)، فلاتر متد زیر را روی کنترلر اجرا می‌کند: ```dart void sendEventToWeb(WebViewController controller, String type, Map payload) { final event = { "type": type, "payload": payload, }; final jsonStr = jsonEncode(event); controller.runJavaScript(''' window.dispatchEvent(new CustomEvent('flutterEvent', { detail: $jsonStr })); '''); } ``` ### نمونه رویدادها: 1. **چرخش گوشی (`ORIENTATION_CHANGED`):** ```dart sendEventToWeb(controller, "ORIENTATION_CHANGED", { "orientation": isPortrait ? "portrait" : "landscape", "width": size.width, "height": size.height, }); ``` 2. **تغییر تم اپ (`THEME_CHANGED`):** ```dart sendEventToWeb(controller, "THEME_CHANGED", { "mode": isDark ? "dark" : "light", }); ``` --- ## ۶. پیشنهادات UX و چرخش صفحه در فلاتر > [!TIP] > **پیشنهاد بهینه‌سازی تجربه کاربری (Orientation):** > با توجه به اینکه نمودارهای درختی و Mind-Map در حالت افقی (Landscape) بیشترین فضای نمایش را دارند، پیشنهاد می‌شود هنگام ورود به این صفحه: > 1. جهت صفحه به صورت آزاد (هم Portrait و هم Landscape) مجاز باشد: > ```dart > SystemChrome.setPreferredOrientations([ > DeviceOrientation.portraitUp, > DeviceOrientation.landscapeLeft, > DeviceOrientation.landscapeRight, > ]); > ``` > 2. هنگام خروج از صفحه (`dispose`)، تنظیمات جهت صفحه به حالت پیش‌فرض اپلیکیشن بازگردد. --- ## ۷. نمونه کد کامل پیاده‌سازی صفحه در Flutter (`webview_flutter`) ```dart import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:share_plus/share_plus.dart'; class XMindMindMapScreen extends StatefulWidget { final String categorySlug; final String? authToken; final String currentLang; const XMindMindMapScreen({ Key? key, required this.categorySlug, this.authToken, this.currentLang = 'fa', }) : super(key: key); @override State createState() => _XMindMindMapScreenState(); } class _XMindMindMapScreenState extends State { late final WebViewController _controller; bool _isLoading = true; double _loadingProgress = 0; @override void initState() { super.initState(); // Allow both portrait and landscape orientations for better mindmap view SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); _initWebView(); } @override void dispose() { // Reset back to portrait when leaving the screen SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); super.dispose(); } void _initWebView() { final isDark = false; // Or fetch from ThemeProvider final baseUri = Uri.parse('https://dovodi.newhorizonco.uk/xmind/${widget.categorySlug}'); final queryParams = { 'platform': 'flutter', 'lang': widget.currentLang, 'theme': isDark ? 'dark' : 'light', }; if (widget.authToken != null && widget.authToken!.isNotEmpty) { queryParams['token'] = widget.authToken!; } final finalUri = baseUri.replace(queryParameters: queryParams); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0xFFF8FAFC)) ..addJavaScriptChannel( 'HabibApp', onMessageReceived: (JavaScriptMessage message) { _handleWebMessage(message.message); }, ) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { setState(() { _loadingProgress = progress / 100.0; }); }, onPageStarted: (String url) { setState(() => _isLoading = true); }, onPageFinished: (String url) { setState(() => _isLoading = false); _sendInitialConfig(); }, ), ) ..loadRequest(finalUri); } void _sendInitialConfig() { final mediaQuery = MediaQuery.of(context); final isDark = Theme.of(context).brightness == Brightness.dark; final config = { "version": "1.0.0", "platform": { "os": Platform.isAndroid ? "android" : "ios", "isFlutter": true, }, "theme": { "mode": isDark ? "dark" : "light", }, "safeArea": { "top": mediaQuery.padding.top, "bottom": mediaQuery.padding.bottom, "left": mediaQuery.padding.left, "right": mediaQuery.padding.right, }, "screen": { "width": mediaQuery.size.width, "height": mediaQuery.size.height, "orientation": mediaQuery.orientation == Orientation.portrait ? "portrait" : "landscape", } }; _controller.runJavaScript(''' window.dispatchEvent(new CustomEvent('flutterConfig', { detail: ${jsonEncode(config)} })); '''); } void _handleWebMessage(String rawJson) { try { final data = jsonDecode(rawJson) as Map; final action = data['action'] as String?; final payload = (data['data'] ?? data['payload'] ?? {}) as Map; switch (action) { case 'close_webview': case 'CLOSE_XMIND_VIEW': Navigator.of(context).pop(); break; case 'share_content': case 'SHARE_CONTENT': final text = payload['text'] as String? ?? ''; final url = payload['url'] as String? ?? ''; final title = payload['title'] as String? ?? ''; Share.share('$title\n$text\n$url'); break; case 'copy_to_clipboard': case 'COPY_TO_CLIPBOARD': final text = payload['text'] as String? ?? ''; if (text.isNotEmpty) { Clipboard.setData(ClipboardData(text: text)); HapticFeedback.lightImpact(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('کپی شد')), ); } break; case 'download_file': case 'DOWNLOAD_XMIND': final url = payload['url'] as String? ?? ''; final fileName = payload['fileName'] as String? ?? 'MindMap.xmind'; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('در حال دانلود $fileName...')), ); break; default: debugPrint('[WebViewBridge] Unhandled action: $action'); } } catch (e) { debugPrint('[WebViewBridge] Error parsing message: $e'); } } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { if (await _controller.canGoBack()) { _controller.goBack(); return false; } return true; }, child: Scaffold( backgroundColor: const Color(0xFFF8FAFC), body: SafeArea( bottom: false, child: Stack( children: [ WebViewWidget(controller: _controller), if (_isLoading) LinearProgressIndicator( value: _loadingProgress, backgroundColor: Colors.transparent, valueColor: const AlwaysStoppedAnimation(Color(0xFF5172E1)), ), ], ), ), ), ); } } ``` --- ## ۸. چک‌لیست نهایی تست برای توسعه‌دهنده فلاتر - [ ] ارسال پارامتر `platform=flutter` در URL برای پنهان کردن خودکار هدر و فوتر عمومی سایت. - [ ] تست باز شدن مایند‌مپ و پیمایش لمسی (Pinch to Zoom و Pan). - [ ] تست چرخش صفحه به حالت افقی (Landscape) جهت مشاهده بهینه شاخه‌های درختی. - [ ] تست دریافت رویداد `close_webview` و برگشت سریع به صفحه دسته‌بندی در فلاتر. - [ ] تست اشتراک‌گذاری محتوای کارت با `share_content`. - [ ] تست ارسال توکن لاگین جهت فعال بودن نشان‌گذاری‌ها (Bookmarks).