diff --git a/docs/FLUTTER_XMIND_INTEGRATION_GUIDE.md b/docs/FLUTTER_XMIND_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..a5345f8 --- /dev/null +++ b/docs/FLUTTER_XMIND_INTEGRATION_GUIDE.md @@ -0,0 +1,387 @@ +# 🧠 راهنمای جامع اتصال و پیاده‌سازی صفحه 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).