20 KiB
📊 نحوه Navigation به هر سرویس
1️⃣ قرآن (Quran)
A. باز کردن سرویس قرآن (صفحه اصلی):
Navigator.pushNamed(context, RoutesName.quran);
// از WebView:
window.FlutterChannel?.postMessage(JSON.stringify({
type: 'NAVIGATE',
payload: {
route: '/quran'
}
}));
B. باز کردن صفحه خاص (آیه):
// Navigation در کد فلاتر:
context.pushPage(QuranAyasScreen(ayaId));
// مثالها:
context.pushPage(QuranAyasScreen(123)); // آیه 123
context.pushPage(QuranAyasScreen(null)); // از اول
context.pushPage(QuranAyasScreen(
surah.startAt, // آیه اول سوره
));
ساختار QuranAyasScreen:
class QuranAyasScreen extends StatefulWidget {
final int? startAya; // ⭐ ID آیه
final String? autoPlayReciter; // ⭐ پخش خودکار (اختیاری)
const QuranAyasScreen(
this.startAya,
{this.autoPlayReciter, super.key}
);
}
JSON برای WebView:
{
"type": "NAVIGATE_QURAN",
"payload": {
"ayaId": 123,
"autoPlay": true,
"reciterId": "abdul-basit"
}
}
یا سادهتر:
{
"type": "NAVIGATE",
"payload": {
"route": "/quran",
"params": {
"ayaId": 123,
"autoPlay": true
}
}
}
2️⃣ مفاتیح (Mafatih)
A. باز کردن سرویس مفاتیح (صفحه اصلی):
Navigator.pushNamed(context, RoutesName.mafatih);
// از WebView:
window.FlutterChannel?.postMessage(JSON.stringify({
type: 'NAVIGATE',
payload: {
route: '/mafatih'
}
}));
B. باز کردن دعای خاص:
// Navigation در کد فلاتر:
context.pushPage(MafatihDuasScreen(duaId));
// مثال:
context.pushPage(MafatihDuasScreen(45)); // دعای عهد
context.pushPage(MafatihDuasScreen(1)); // دعای اول
ساختار MafatihDuasScreen:
class MafatihDuasScreen extends StatefulWidget {
final int? startDuaId; // ⭐ ID دعا
final int? startDuaPartId; // ⭐ بخش خاص دعا (اختیاری)
const MafatihDuasScreen(
this.startDuaId,
{this.startDuaPartId, super.key}
);
}
JSON برای WebView:
{
"type": "NAVIGATE_MAFATIH",
"payload": {
"duaId": 45,
"duaPartId": 2,
"autoPlay": true
}
}
3️⃣ کتابخانه (Library)
A. باز کردن سرویس کتابخانه (صفحه اصلی):
Navigator.pushNamed(context, RoutesName.library);
// از WebView:
window.FlutterChannel?.postMessage(JSON.stringify({
type: 'NAVIGATE',
payload: {
route: '/library'
}
}));
B. باز کردن کتاب خاص:
// Navigation در کد فلاتر:
context.pushPage(BookPage(bookSlug));
// یا با named route:
Navigator.pushNamed(
context,
RoutesName.librarySinglePage,
arguments: bookSlug,
);
JSON برای WebView:
{
"type": "NAVIGATE_LIBRARY",
"payload": {
"slug": "kitab-al-tawheed"
}
}
توضیحات:
slug: شناسه یکتای متنی کتاب (مثال:"kitab-al-tawheed")- API Endpoint:
GET /library/v2/books/{slug}/
4️⃣ حسینیه (Hossienieh)
A. باز کردن سرویس حسینیه:
Navigator.pushNamed(context, RoutesName.hosseinieh);
// از WebView:
window.FlutterChannel?.postMessage(JSON.stringify({
type: 'NAVIGATE',
payload: {
route: '/hosseinieh'
}
}));
B. باز کردن پلیر با آهنگ:
// Navigation در کد فلاتر:
context.read<MusicPlayerCubit>().openAudio(
audios: [song1, song2, song3],
songModel: currentSong,
showSmallPlayer: true,
);
// با تنظیمات پیشرفته (پیشنهادی جدید):
context.read<MusicPlayerCubit>().openHosseiniehPlaylist(
HosseiniehPlaylist(
audios: audios,
startIndex: 0,
startDuration: 120000, // شروع از دقیقه 2 (میلیثانیه)
playbackSpeed: 2.0, // سرعت 2 برابر
),
showSmallPlayer: true,
);
JSON برای WebView (پیشنهاد جدید):
{
"type": "PLAY_HOSSEINIEH",
"payload": {
"songs": [
{
"slug": "song-123",
"title": "نوحه محرم",
"singers": [{"name": "حاج محمود کریمی"}],
"file": {"audio": "https://example.com/audio.mp3"},
"thumbnail": {"sm": "https://example.com/thumb.jpg"}
}
],
"currentSongSlug": "song-123",
"showSmallPlayer": true,
"config": {
"startTime": 120000,
"playbackSpeed": 2.0,
"loopMode": "single",
"autoPlay": true,
"fadeInDuration": 1000,
"fadeOutDuration": 1000
}
}
}
توضیح فیلدهای config:
| فیلد | نوع | پیشفرض | توضیح |
|---|---|---|---|
startTime |
number |
0 |
شروع از زمان مشخص (میلیثانیه) - مثل مفاتیح |
playbackSpeed |
number |
1.0 |
سرعت پخش (0.5x تا 2.0x) |
loopMode |
"none" | "single" | "playlist" |
"none" |
حالت تکرار |
autoPlay |
boolean |
true |
شروع خودکار پخش |
fadeInDuration |
number? |
- | مدت زمان fade-in (میلیثانیه) |
fadeOutDuration |
number? |
- | مدت زمان fade-out (میلیثانیه) |
pitch |
number? |
1.0 |
تغییر pitch (اختیاری) |
volume |
number? |
1.0 |
حجم صدا (0.0 تا 1.0) |
مثالهای کاربردی:
۱. شروع از دقیقه 2 با سرعت 2 برابر:
{
"type": "PLAY_HOSSEINIEH",
"payload": {
"songs": [...],
"currentSongSlug": "song-123",
"config": {
"startTime": 120000,
"playbackSpeed": 2.0
}
}
}
۲. پخش کامل با fade-in:
{
"type": "PLAY_HOSSEINIEH",
"payload": {
"songs": [...],
"currentSongSlug": "song-123",
"config": {
"fadeInDuration": 2000,
"loopMode": "single"
}
}
}
۳. شروع از انتخاب کاربر با تنظیمات پیشرفته:
{
"type": "PLAY_HOSSEINIEH",
"payload": {
"songs": [...],
"currentSongSlug": "song-456",
"config": {
"startTime": 0,
"playbackSpeed": 1.5,
"volume": 0.8,
"autoPlay": true
}
}
}
6️⃣ تاک (Talk)
A. باز کردن صفحه اصلی Talk (لیست مشاورین):
// Navigation در کد فلاتر:
Navigator.pushNamed(context, RoutesName.talk);
JSON برای WebView:
{
"type": "NAVIGATE_TALK"
}
توضیحات:
- باز میکند صفحه اصلی سرویس Talk
- برای کاربر عادی: لیست مشاورین نمایش داده میشود
- برای مشاور: داشبورد مشاور نمایش داده میشود
B. باز کردن صفحه جزئیات مشاور:
// Navigation در کد فلاتر:
context.pushPage(ConsultantInfoPage(consultant));
// با استفاده از notification:
Navigator.pushNamed(
context,
'/TalkConsultantPage',
arguments: consultantJson,
);
JSON برای WebView:
{
"type": "NAVIGATE_TALK",
"payload": {
"page": "consultant",
"username": "consultant_username"
}
}
توضیحات:
username: نام کاربری یکتای مشاور (مثال:"ahmad_consultant")- صفحه جزئیات شامل: بیوگرافی، نظرات، زمانبندی و انواع تماس
- از این صفحه کاربر میتواند درخواست چت یا تماس بدهد
نمونه Consultant JSON کامل:
{
"username": "consultant_username",
"fullname": "احمد محمدی",
"avatar_url": "https://example.com/avatar.jpg",
"slogan": "مشاور خانواده و ازدواج",
"bio": "توضیحات کامل مشاور...",
"status": "online",
"unread_count": 0,
"avg_rate": 4.5,
"languages": ["fa", "en"],
"contact_type": ["chat", "voice", "video"],
"categories": [],
"topics": ["ازدواج", "خانواده"],
"session_duration": 30,
"video_call_cost": 50000,
"voice_call_cost": 30000,
"first_message": "سلام، چطور میتونم کمکتون کنم؟",
"is_ai": false
}
C. باز کردن صفحه چت با مشاور:
// Navigation در کد فلاتر:
context.pushPage(TalkChatPage(event: callEvent));
// با استفاده از notification:
Navigator.pushNamed(
context,
'/TalkChatPage',
arguments: callEventJson,
);
JSON برای WebView:
{
"type": "NAVIGATE_TALK",
"payload": {
"page": "chat",
"call_id": 12345,
"uuid": "consultant_username",
"call_type": "chat",
"from_user_username": "consultant_username",
"from_user_fullname": "احمد محمدی",
"from_user_avatar": "https://example.com/avatar.jpg",
"session_duration": 30,
"price": 0,
"is_from_user": true
}
}
توضیحات:
call_id: شناسه یکتای چت/تماس (اختیاری)uuid: نام کاربری مشاورcall_type: نوع تماس ("chat","voice", یا"video")from_user_*: اطلاعات فرستنده پیامsession_duration: مدت زمان جلسه به دقیقهprice: هزینه تماس (برای چت معمولاً 0)is_from_user: آیا از طرف کاربر است یا مشاور
نمونه CallEvent JSON کامل:
{
"act": "",
"call_id": 12345,
"uuid": "consultant_username",
"call_type": "chat",
"session_duration": 30,
"call_direction": "outgoing",
"is_from_user": true,
"price": 0,
"me": {
"name": "محمد رضایی",
"username": "user_username",
"avatar": "https://example.com/user-avatar.jpg"
},
"contact": {
"name": "احمد محمدی",
"username": "consultant_username",
"avatar": "https://example.com/consultant-avatar.jpg",
"extra": "مشاور خانواده"
},
"chat_data": {
"init_text": "سلام، نیاز به مشاوره دارم",
"first_message": "سلام، چطور میتونم کمکتون کنم؟"
}
}
D. نکات مهم برای دو طرفه بودن Navigation:
1. از کاربر به مشاور (User → Consultant):
- کاربر از لیست مشاورین (
UserHomePage) میتواند:- به صفحه جزئیات مشاور (
ConsultantInfoPage) برود - از آنجا درخواست چت بدهد و به صفحه چت (
TalkChatPage) برود
- به صفحه جزئیات مشاور (
2. از مشاور به کاربر (Consultant → User):
- مشاور از داشبورد خود (
ConsultantHomePage) میتواند:- لیست چتها را ببیند (
ChatsPage) - روی هر چت کلیک کند و به صفحه چت (
TalkChatPage) برود - درخواستهای booking را ببیند (
BookingPage)
- لیست چتها را ببیند (
3. نوتیفیکیشنها:
- هر دو طرف از طریق notification میتوانند مستقیماً به چت بروند
- Navigation routes:
/TalkChatPage→ باز کردن صفحه چت/TalkConsultantPage→ باز کردن صفحه جزئیات مشاور
7️⃣ پلیر کنترل (Player Control Events)
کنترلهای استاندارد:
{
"type": "PLAYER_PAUSE"
}
{
"type": "PLAYER_RESUME"
}
{
"type": "PLAYER_STOP"
}
{
"type": "PLAYER_SEEK",
"payload": {
"position": 60000
}
}
{
"type": "PLAYER_SET_SPEED",
"payload": {
"speed": 1.5
}
}
{
"type": "PLAYER_SET_VOLUME",
"payload": {
"volume": 0.7
}
}
{
"type": "PLAYER_NEXT"
}
{
"type": "PLAYER_PREVIOUS"
}
9️⃣ Cross-Service Navigation (نویگیشن بین سرویسها)
این event برای انتقال از یک سرویس به سرویس دیگر استفاده میشود. هم برای Native Services و هم WebView Services کار میکند.
A. Navigation به سرویس Native:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "quran",
"params": {
"ayaId": 123,
"autoPlay": true
}
}
}
سرویسهای Native موجود:
quran→ سرویس قرآنmafatih→ سرویس مفاتیحlibrary→ کتابخانهhosseinieh→ حسینیهtalk→ Talk (مشاوره)meet→ Meethabit→ عاداتahkaam→ احکامtafsir→ تفسیر
B. Navigation به WebView Service با URL کامل:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "marriage",
"url": "https://marriage.habibapp.com",
"path": "/games",
"params": {
"level": 5,
"mode": "challenge"
}
}
}
توضیحات:
service: نام سرویس (برای لاگ و tracking)url: آدرس پایه WebViewpath: مسیر درون سرویس (اختیاری)params: پارامترهای query string (اختیاری)
URL نهایی:
https://marriage.habibapp.com/games?level=5&mode=challenge
C. Navigation به WebView با Service Name (از Config):
اگر سرویس از قبل در ServiceConfigs تعریف شده باشد:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "marriage",
"path": "/games",
"params": {
"level": 5
}
}
}
توضیحات:
- Flutter از
ServiceConfigsآدرس پایه را میخواند pathوparamsبه آدرس پایه اضافه میشوند
D. مثالهای کاربردی:
۱. از سرویس Marriage به Talk:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "talk",
"params": {
"page": "consultant",
"username": "marriage_consultant"
}
}
}
۲. از Talk به بازی Marriage:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "marriage",
"path": "/games/compatibility",
"params": {
"consultantId": "ahmad_consultant"
}
}
}
۳. از Quran به Library (کتاب تفسیر):
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "library",
"params": {
"slug": "tafsir-al-mizan",
"chapter": 5
}
}
}
۴. از Library به Mafatih:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "mafatih",
"params": {
"duaId": 45
}
}
}
۵. از WebView به WebView دیگر:
{
"type": "NAVIGATE_TO_SERVICE",
"payload": {
"service": "health",
"url": "https://health.habibapp.com",
"path": "/consultation",
"params": {
"type": "mental_health",
"referrer": "marriage"
}
}
}
E. پیادهسازی در Flutter:
Future<void> handleNavigateToService(Map<String, dynamic> payload) async {
final serviceName = payload['service'] as String;
final params = payload['params'] as Map<String, dynamic>?;
final path = payload['path'] as String?;
final url = payload['url'] as String?;
// Native Services
if (_isNativeService(serviceName)) {
return _navigateToNativeService(serviceName, params);
}
// WebView Services
final serviceUrl = url ?? _getServiceUrlFromConfig(serviceName);
if (serviceUrl != null) {
final fullUrl = _buildWebViewUrl(serviceUrl, path, params);
return _navigateToWebView(serviceName, fullUrl);
}
// Service not found
throw Exception('Service "$serviceName" not found');
}
String _buildWebViewUrl(
String baseUrl,
String? path,
Map<String, dynamic>? params,
) {
final uri = Uri.parse(baseUrl);
final pathSegment = path ?? '';
final queryParams = params?.map(
(key, value) => MapEntry(key, value.toString()),
) ?? {};
return uri.replace(
path: pathSegment,
queryParameters: queryParams.isEmpty ? null : queryParams,
).toString();
}
F. استفاده در React/TypeScript:
// utils/navigation.ts
interface NavigateToServicePayload {
service: string;
url?: string;
path?: string;
params?: Record<string, any>;
}
export function navigateToService(payload: NavigateToServicePayload) {
window.FlutterChannel?.postMessage(JSON.stringify({
type: 'NAVIGATE_TO_SERVICE',
payload,
}));
}
// مثال استفاده:
import { navigateToService } from '@/utils/navigation';
// Navigate to Talk
navigateToService({
service: 'talk',
params: {
page: 'consultant',
username: 'consultant_username',
},
});
// Navigate to another WebView service
navigateToService({
service: 'marriage',
path: '/games',
params: { level: 5 },
});
G. Schema کامل:
interface NavigateToServiceEvent {
type: 'NAVIGATE_TO_SERVICE';
payload: {
// Required: نام سرویس
service: string;
// Optional: برای WebView services
url?: string; // آدرس کامل پایه
path?: string; // مسیر داخل سرویس
params?: Record<string, any>; // Query parameters
// Optional: تنظیمات اضافی
options?: {
replaceCurrentRoute?: boolean; // جایگزین route فعلی
clearStack?: boolean; // پاک کردن history
animation?: 'slide' | 'fade' | 'none';
};
};
}
H. نکات مهم:
-
Service Name:
- باید lowercase باشد
- فقط حروف، اعداد و underscore
- مثال:
marriage,health_tips,quran_v2
-
URL Building:
- اگر
urlوجود نداشته باشد، ازServiceConfigsخوانده میشود pathباید با/شروع شودparamsبه صورت خودکار به query string تبدیل میشود
- اگر
-
Security:
- فقط URLهای مجاز (
habibapp.com) قابل باز شدن هستند - پارامترها باید sanitize شوند
- فقط URLهای مجاز (
-
History Management:
- هر navigation به history stack اضافه میشود
- کاربر میتواند با دکمه back برگردد
Eventهای ارسال شده از Flutter:
// types/player-events.ts
interface PlayerStateEvent {
type: 'PLAYER_STATE_CHANGED';
payload: {
isPlaying: boolean;
currentAudio: {
id: string;
title: string;
artist?: string;
duration: number;
thumbnail?: string;
};
currentPosition: number;
duration: number;
playbackSpeed: number;
volume: number;
};
}
interface PlayerProgressEvent {
type: 'PLAYER_PROGRESS';
payload: {
position: number;
duration: number;
buffered: number;
};
}
interface PlayerErrorEvent {
type: 'PLAYER_ERROR';
payload: {
code: string;
message: string;
};
}
دریافت در React:
// hooks/usePlayerState.ts
import { useEffect, useState } from 'react';
export function usePlayerState() {
const [isPlaying, setIsPlaying] = useState(false);
const [currentAudio, setCurrentAudio] = useState<any>(null);
const [position, setPosition] = useState(0);
const [duration, setDuration] = useState(0);
const [speed, setSpeed] = useState(1.0);
useEffect(() => {
const handlePlayerState = (event: CustomEvent) => {
const { type, payload } = event.detail;
switch (type) {
case 'PLAYER_STATE_CHANGED':
setIsPlaying(payload.isPlaying);
setCurrentAudio(payload.currentAudio);
setSpeed(payload.playbackSpeed);
break;
case 'PLAYER_PROGRESS':
setPosition(payload.position);
setDuration(payload.duration);
break;
case 'PLAYER_ERROR':
console.error('Player error:', payload.message);
break;
}
};
window.addEventListener('playerState', handlePlayerState as EventListener);
return () => {
window.removeEventListener('playerState', handlePlayerState as EventListener);
};
}, []);
return {
isPlaying,
currentAudio,
position,
duration,
speed,
};
}