You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

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 → Meet
  • habit → عادات
  • 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: آدرس پایه WebView
  • path: مسیر درون سرویس (اختیاری)
  • 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. نکات مهم:

  1. Service Name:

    • باید lowercase باشد
    • فقط حروف، اعداد و underscore
    • مثال: marriage, health_tips, quran_v2
  2. URL Building:

    • اگر url وجود نداشته باشد، از ServiceConfigs خوانده می‌شود
    • path باید با / شروع شود
    • params به صورت خودکار به query string تبدیل می‌شود
  3. Security:

    • فقط URLهای مجاز (habibapp.com) قابل باز شدن هستند
    • پارامترها باید sanitize شوند
  4. 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,
  };
}