Browse Source

docs(xmind): add Flutter WebView & PWA integration guide for XMind mind-map page

master
Mohsen Taba 3 weeks ago
parent
commit
3924ead9d1
  1. 387
      docs/FLUTTER_XMIND_INTEGRATION_GUIDE.md

387
docs/FLUTTER_XMIND_INTEGRATION_GUIDE.md

@ -0,0 +1,387 @@
# 🧠 راهنمای جامع اتصال و پیاده‌سازی صفحه XMind (Mind-Map) در اپلیکیشن Flutter
این مستند فنی برای برنامه‌نویسان فلاتر (Flutter) آماده شده است تا بتوانند صفحه نمایش نمودار درختی و مایند‌مپ احادیث و ارگومان‌ها (`/xmind/<category_slug>`) را با بهترین عملکرد، ظاهر بومی (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<String, dynamic> 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<XMindMindMapScreen> createState() => _XMindMindMapScreenState();
}
class _XMindMindMapScreenState extends State<XMindMindMapScreen> {
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 = <String, String>{
'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<String, dynamic>;
final action = data['action'] as String?;
final payload = (data['data'] ?? data['payload'] ?? {}) as Map<String, dynamic>;
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>(Color(0xFF5172E1)),
),
],
),
),
),
);
}
}
```
---
## ۸. چک‌لیست نهایی تست برای توسعه‌دهنده فلاتر
- [ ] ارسال پارامتر `platform=flutter` در URL برای پنهان کردن خودکار هدر و فوتر عمومی سایت.
- [ ] تست باز شدن مایند‌مپ و پیمایش لمسی (Pinch to Zoom و Pan).
- [ ] تست چرخش صفحه به حالت افقی (Landscape) جهت مشاهده بهینه شاخه‌های درختی.
- [ ] تست دریافت رویداد `close_webview` و برگشت سریع به صفحه دسته‌بندی در فلاتر.
- [ ] تست اشتراک‌گذاری محتوای کارت با `share_content`.
- [ ] تست ارسال توکن لاگین جهت فعال بودن نشان‌گذاری‌ها (Bookmarks).
Loading…
Cancel
Save