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.
 
 
 
 

30 KiB

WebView Utility Actions

این سند ساختار actionهای عمومی WebView به Flutter را تعریف می‌کند. این actionها برای کارهایی هستند که بهتر است به‌صورت native توسط Flutter انجام شوند: دانلود فایل، آپلود فایل (انتخاب فایل/تصویر/ویدیو از دستگاه)، کپی متن در clipboard دستگاه، و باز کردن لینک در مرورگر/اپ خارجی کاربر.

کانال ارتباطی

وب باید از کانال فعلی WebView استفاده کند:

HabibApp.postMessage(JSON.stringify(message));

Flutter هم مثل سایر actionها پاسخ را از این مسیر به Web برمی‌گرداند:

window.onFlutterResponse(response);

۱. دانلود فایل

هدف

وب نباید خودش فایل را داخل WebView دانلود کند. وقتی کاربر روی دکمه دانلود هر فایلی کلیک کرد، وب باید یک پیام JSON به Flutter بفرستد تا Flutter دانلود native را برای کاربر انجام دهد.

Action اصلی

نام action باید عمومی باشد:

download_file

از download_audio استفاده نشود، چون فقط یک نوع فایل را پوشش می‌دهد.

پیام ارسالی از Web به Flutter

{
  "action": "download_file",
  "data": {
    "url": "https://example.com/files/sample.pdf",
    "fileName": "sample.pdf",
    "title": "Sample File",
    "mimeType": "application/pdf",
    "fileType": "document"
  }
}

فیلدهای پیام

Field Required Type توضیح
action بله string مقدار ثابت: download_file
data.url بله string لینک مستقیم دانلود فایل. باید https یا http باشد
data.fileName پیشنهاد می‌شود string نام فایل ذخیره‌شده روی دستگاه، همراه extension
data.title اختیاری string عنوان قابل نمایش در UI/Notification
data.mimeType پیشنهاد می‌شود string MIME type فایل، مثل image/jpeg, video/mp4, audio/mpeg, application/pdf
data.fileType اختیاری string دسته‌بندی ساده برای UI، مثل image, video, audio, document, archive, other
data.size اختیاری number حجم فایل به byte اگر از قبل مشخص است
data.metadata اختیاری object اطلاعات اضافه، مثل artist, duration, thumbnailUrl

fileTypeهای پیشنهادی

fileType نمونه mimeType نمونه extension
image image/jpeg, image/png, image/webp .jpg, .png, .webp
video video/mp4, video/quicktime .mp4, .mov
audio audio/mpeg, audio/mp4, audio/wav .mp3, .m4a, .wav
document application/pdf, text/plain .pdf, .txt
archive application/zip, application/x-rar-compressed .zip, .rar
other هر MIME type دیگر هر extension معتبر

مثال‌ها

دانلود تصویر

function downloadImage(image) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'download_file',
    data: {
      url: image.url,
      fileName: `${image.slug}.jpg`,
      title: image.title,
      mimeType: 'image/jpeg',
      fileType: 'image'
    }
  }));
}

دانلود ویدیو

function downloadVideo(video) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'download_file',
    data: {
      url: video.downloadUrl,
      fileName: `${video.slug}.mp4`,
      title: video.title,
      mimeType: 'video/mp4',
      fileType: 'video',
      metadata: {
        duration: video.duration,
        thumbnailUrl: video.thumbnailUrl
      }
    }
  }));
}

دانلود فایل صوتی

function downloadAudio(song) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'download_file',
    data: {
      url: song.downloadUrl,
      fileName: `${song.slug}.mp3`,
      title: song.title,
      mimeType: 'audio/mpeg',
      fileType: 'audio',
      metadata: {
        artist: song.artist,
        duration: song.duration
      }
    }
  }));
}

دانلود PDF

function downloadPdf(book) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'download_file',
    data: {
      url: book.pdfUrl,
      fileName: `${book.slug}.pdf`,
      title: book.title,
      mimeType: 'application/pdf',
      fileType: 'document'
    }
  }));
}

نمونه استفاده روی دکمه دانلود

<button onClick={() => downloadVideo(video)}>
  Download
</button>

شروع دانلود

{
  "action": "download_file",
  "success": true,
  "status": "started",
  "data": {
    "fileName": "sample.pdf",
    "fileType": "document",
    "mimeType": "application/pdf"
  }
}

پیشرفت دانلود

{
  "action": "download_file",
  "success": true,
  "status": "progress",
  "data": {
    "progress": 42,
    "received": 420000,
    "total": 1000000,
    "fileName": "sample.pdf"
  }
}

اتمام دانلود

{
  "action": "download_file",
  "success": true,
  "status": "completed",
  "data": {
    "fileName": "sample.pdf",
    "fileType": "document",
    "mimeType": "application/pdf"
  }
}

خطا در دانلود

{
  "action": "download_file",
  "success": false,
  "status": "failed",
  "message": "Invalid download url"
}

مدیریت Response در Web

window.onFlutterResponse = function (response) {
  if (response.action !== 'download_file') return;

  if (response.status === 'started') {
    // UI را به حالت downloading ببرید
  }

  if (response.status === 'progress') {
    // response.data.progress عدد 0 تا 100 است
  }

  if (response.status === 'completed') {
    // پیام موفقیت نمایش دهید
  }

  if (response.status === 'failed') {
    // پیام خطا نمایش دهید
  }
};

نکات مهم برای Web

  • لینک data.url باید مستقیم به فایل برسد، نه صفحه HTML.
  • data.fileName باید extension داشته باشد، مثل .jpg, .mp4, .mp3, .pdf, .zip.
  • اگر fileName ارسال نشود، Flutter می‌تواند نام فایل را از URL استخراج کند، اما ارسال fileName بهتر است.
  • اگر فایل نیاز به authorization دارد، بهتر است لینک دانلود signed/temporary باشد یا Flutter بتواند با کوکی/توکن فعلی آن را دریافت کند.
  • mimeType و fileType برای UI، notification و انتخاب روش ذخیره‌سازی مفیدند؛ بهتر است ارسال شوند.
  • وب فقط درخواست دانلود را می‌فرستد؛ ذخیره فایل، permission، notification، انتخاب مسیر و مدیریت platform بر عهده Flutter است.

سازگاری با نام قدیمی

اگر قبلا در Web از download_audio استفاده شده، بهتر است به download_file مهاجرت کند.

پیشنهاد برای Flutter:

case 'download_file':
  return _handleDownloadFile(data);

در صورت نیاز موقت به backward compatibility:

case 'download_audio':
case 'download_file':
  return _handleDownloadFile(data);

اما قرارداد نهایی و مستند باید download_file باشد.


۲. کپی متن در Clipboard کاربر

هدف

وب می‌تواند navigator.clipboard.writeText(...) را امتحان کند، اما در WebView مخصوصاً روی iOS/Android همیشه قابل اتکا نیست و ممکن است به user gesture، permission یا secure context وابسته باشد. برای رفتار مطمئن داخل اپ، Web باید متن را با action به Flutter بدهد و Flutter متن را در clipboard دستگاه کپی کند.

Action اصلی

copy_to_clipboard

پیام ارسالی از Web به Flutter

{
  "action": "copy_to_clipboard",
  "data": {
    "text": "متنی که باید کپی شود",
    "label": "invite_code"
  }
}

فیلدهای پیام

Field Required Type توضیح
action بله string مقدار ثابت: copy_to_clipboard
data.text بله string متن قابل کپی
data.label اختیاری string نام یا context برای analytics/debug، مثل invite_code, share_link
data.showToast اختیاری boolean اگر true باشد Flutter می‌تواند پیام موفقیت نشان دهد. مقدار پیش‌فرض پیشنهادی: true

مثال JavaScript

function copyText(text) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'copy_to_clipboard',
    data: {
      text,
      label: 'share_text',
      showToast: true
    }
  }));
}

نمونه استفاده روی دکمه

<button onClick={() => copyText(inviteCode)}>
  Copy Code
</button>

Response موفق

{
  "action": "copy_to_clipboard",
  "success": true,
  "status": "completed",
  "data": {
    "label": "share_text"
  }
}

Response خطا

{
  "action": "copy_to_clipboard",
  "success": false,
  "status": "failed",
  "message": "Text is empty"
}

مدیریت Response در Web

window.onFlutterResponse = function (response) {
  if (response.action !== 'copy_to_clipboard') return;

  if (response.success) {
    // UI کپی موفق را نمایش دهید
  } else {
    // پیام خطا نمایش دهید
  }
};

پیشنهاد fallback سمت Web

اگر Web خارج از اپ هم اجرا می‌شود، می‌تواند helper عمومی داشته باشد:

async function copyToClipboard(text) {
  if (window.HabibApp) {
    window.HabibApp.postMessage(JSON.stringify({
      action: 'copy_to_clipboard',
      data: { text, showToast: true }
    }));
    return;
  }

  await navigator.clipboard.writeText(text);
}

نکات مهم برای Web

  • data.text نباید خالی باشد.
  • برای متن‌های حساس مثل token یا اطلاعات شخصی، قبل از ارسال به clipboard حتما UX واضح داشته باشید.
  • اگر Web داخل مرورگر عادی اجرا شود و HabibApp وجود نداشته باشد، از Clipboard API مرورگر استفاده شود.

۳. باز کردن لینک در مرورگر یا اپ خارجی کاربر

هدف

اگر داخل WebView دکمه‌ای وجود دارد که باید یک آدرس را خارج از WebView باز کند، Web باید به Flutter پیام بدهد تا Flutter با url_launcher لینک را در مرورگر/اپ مناسب باز کند. این برای لینک‌های خارجی، پرداخت، نقشه، تلگرام، واتساپ، YouTube و صفحات وب عمومی کاربرد دارد.

Action اصلی

open_external_url

پیام ارسالی از Web به Flutter

{
  "action": "open_external_url",
  "data": {
    "url": "https://example.com/page",
    "mode": "externalApplication",
    "title": "Open Website"
  }
}

فیلدهای پیام

Field Required Type توضیح
action بله string مقدار ثابت: open_external_url
data.url بله string آدرس مقصد
data.mode اختیاری string روش باز کردن. مقدار پیشنهادی: externalApplication
data.title اختیاری string عنوان برای analytics/debug یا UI
data.showErrorToast اختیاری boolean اگر باز کردن لینک fail شد Flutter پیام خطا نشان دهد

URL schemeهای قابل قبول

Scheme کاربرد
https / http باز کردن صفحه وب در مرورگر
mailto باز کردن email client
tel باز کردن dialer
sms باز کردن SMS
geo باز کردن نقشه روی Android
custom schemes مثل whatsapp://, tg:// در صورت پشتیبانی دستگاه

مثال JavaScript برای لینک وب

function openInBrowser(url) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'open_external_url',
    data: {
      url,
      mode: 'externalApplication',
      showErrorToast: true
    }
  }));
}

نمونه استفاده روی دکمه

<button onClick={() => openInBrowser('https://habibapp.com/marriage/plans')}>
  Open in Browser
</button>

مثال برای واتساپ

function openWhatsapp(phone) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'open_external_url',
    data: {
      url: `https://wa.me/${phone}`,
      mode: 'externalApplication',
      title: 'whatsapp_contact'
    }
  }));
}

مثال برای تماس تلفنی

function callPhone(phone) {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'open_external_url',
    data: {
      url: `tel:${phone}`,
      mode: 'externalApplication',
      title: 'phone_call'
    }
  }));
}

Response موفق

{
  "action": "open_external_url",
  "success": true,
  "status": "opened",
  "data": {
    "url": "https://example.com/page"
  }
}

Response خطا

{
  "action": "open_external_url",
  "success": false,
  "status": "failed",
  "message": "Cannot launch url"
}

مدیریت Response در Web

window.onFlutterResponse = function (response) {
  if (response.action !== 'open_external_url') return;

  if (!response.success) {
    // پیام خطا یا fallback نمایش دهید
  }
};

نکات مهم برای Web

  • data.url باید valid باشد.
  • برای لینک‌های بیرونی بهتر است https استفاده شود.
  • اگر هدف، باز کردن صفحه داخلی همین سرویس WebView است، از navigation داخلی Web استفاده کنید، نه open_external_url.
  • اگر هدف، باز کردن سرویس دیگر داخل اپ است، باید از action navigation/deep-link داخلی استفاده شود، نه مرورگر خارجی.
  • این action برای خروج از WebView و باز کردن مرورگر/اپ خارجی است.

۴. آپلود فایل (انتخاب فایل/تصویر/ویدیو از دستگاه)

هدف

وقتی داخل WebView دکمه‌ای مثل «آپلود» / «انتخاب تصویر» / «انتخاب ویدیو» کلیک می‌شود، وب نباید از <input type="file"> خود مرورگر استفاده کند چون داخل WebView (به‌ویژه روی iOS و حتی روی برخی نسخه‌های Android) رفتار یکسان و قابل اتکایی ندارد و ممکن است انتخاب دوربین/گالری، دسترسی به فایل و eventهای change با شکست مواجه شود. به‌جای آن Web یک پیام JSON به Flutter می‌فرستد تا Flutter با image_picker / file_picker انتخاب native را انجام دهد و سپس نتیجه را به Web برگرداند.

دو حالت پاسخ وجود دارد:

  1. حالت آپلود (پیشنهادی، پیش‌فرض): Flutter فایل را انتخاب کرده و خودش با multipart POST به uploadUrl می‌فرستد و فقط URL نهاییِ سرور را به Web برمی‌گرداند. این حالت برای تصویر، ویدیو و فایل‌های بزرگ بهترین گزینه است چون دادهٔ حجیم از کانال JS عبور نمی‌کند.
  2. حالت base64: Flutter فایل را انتخاب کرده و محتوای آن را به‌صورت data: URL به Web برمی‌گرداند. فقط برای تصویرهای کوچک (مثل avatar) مناسب است.

Action اصلی

upload_file

از نام‌های خاص مثل upload_image یا pick_video استفاده نشود؛ upload_file با فیلد mediaType همهٔ نوع‌ها را پوشش می‌دهد.

پیام ارسالی از Web به Flutter

{
  "action": "upload_file",
  "data": {
    "mediaType": "image",
    "source": "gallery",
    "multiple": false,
    "returnAs": "upload",
    "uploadUrl": "https://api.example.com/hussainya/dashboard/upload-media/",
    "fieldName": "file",
    "maxBytes": 10485760,
    "allowedExtensions": ["jpg", "png", "webp"],
    "title": "Profile Avatar"
  }
}

فیلدهای پیام

Field Required Type توضیح
action بله string مقدار ثابت: upload_file
data.mediaType بله string نوع رسانه. مقدارهای مجاز: image, video, image+video, audio, file
data.source اختیاری string منبع انتخاب. مقدارهای مجاز: gallery (پیش‌فرض), camera, any
data.multiple اختیاری boolean انتخاب چند فایل هم‌زمان. پیش‌فرض: false
data.returnAs اختیاری string روش بازگشت نتیجه: upload (پیش‌فرض) یا base64
data.uploadUrl شرطی string در حالت upload اجباری است. endpoint ای که Flutter فایل را با multipart به آن می‌فرستد
data.fieldName اختیاری string نام فیلد form-data. پیش‌فرض: file
data.uploadMethod اختیاری string متد HTTP آپلود. پیش‌فرض: POST
data.headers اختیاری object هدرهای اضافه برای درخواست آپلود (مثل Authorization)؛ Flutter می‌تواند توکن خود را هم تزریق کند
data.maxBytes اختیاری number حداکثر حجم مجاز هر فایل به byte؛ Flutter فایل بزرگ‌تر را رد می‌کند
data.allowedExtensions اختیاری string[] فیلتر extension در حالت file/audio، مثل ["mp3", "wav", "pdf"]
data.maxWidth / data.maxHeight اختیاری number برای image: حداکثر ابعاد تصویر قبل از ارسال (Flutter تصویر را resize می‌کند)
data.imageQuality اختیاری number 0 تا 100 برای فشرده‌سازی تصویر
data.title اختیاری string عنوان برای UI/dialog/analytics

نگاشت mediaType به picker در Flutter

mediaType Flutter picker پیشنهادی
image ImagePicker().pickImage(source: gallery|camera, maxWidth, maxHeight, imageQuality)
video ImagePicker().pickVideo(source: gallery|camera, maxDuration)
image+video ImagePicker().pickMedia(...) یا دو گزینه از sheet
audio FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['mp3',...])
file FilePicker.platform.pickFiles(type: FileType.any | custom, allowedExtensions)

مثال‌ها

انتخاب و آپلود تصویر (حالت پیش‌فرض upload)

function pickAndUploadAvatar() {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'upload_file',
    data: {
      mediaType: 'image',
      source: 'gallery',
      returnAs: 'upload',
      uploadUrl: 'https://api.example.com/hussainya/dashboard/upload-media/',
      fieldName: 'file',
      maxBytes: 5242880,
      maxWidth: 1024,
      imageQuality: 85,
      title: 'Profile Avatar'
    }
  }));
}

انتخاب از دوربین (سلفی/گواهی)

function takePhoto() {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'upload_file',
    data: {
      mediaType: 'image',
      source: 'camera',
      returnAs: 'upload',
      uploadUrl: 'https://api.example.com/hussainya/dashboard/upload-media/',
      fieldName: 'file'
    }
  }));
}

انتخاب ویدیو و آپلود

function pickAndUploadVideo() {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'upload_file',
    data: {
      mediaType: 'video',
      source: 'gallery',
      returnAs: 'upload',
      uploadUrl: 'https://api.example.com/hussainya/dashboard/upload-media/',
      fieldName: 'file',
      title: 'Upload Video'
    }
  }));
}

انتخاب فایل صوتی (mp3)

function pickAudioFile() {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'upload_file',
    data: {
      mediaType: 'audio',
      source: 'file',
      returnAs: 'upload',
      uploadUrl: 'https://api.example.com/hussainya/dashboard/upload-media/',
      allowedExtensions: ['mp3'],
      maxBytes: 26214400,
      title: 'Upload Audio'
    }
  }));
}

حالت base64 (فقط برای تصویر کوچک مثل avatar)

function pickAvatarAsBase64() {
  window.HabibApp?.postMessage(JSON.stringify({
    action: 'upload_file',
    data: {
      mediaType: 'image',
      source: 'gallery',
      returnAs: 'base64',
      maxBytes: 1048576,
      maxWidth: 256,
      imageQuality: 80
    }
  }));
}

نمونه استفاده روی دکمه آپلود

<button onClick={() => pickAndUploadAvatar()}>
  Upload Avatar
</button>

جریان Response

در حالت upload، Flutter چندین response پشت سر هم می‌فرستد (درست مثل download_file):

شروع انتخاب

{
  "action": "upload_file",
  "success": true,
  "status": "picking",
  "data": {
    "mediaType": "image",
    "source": "gallery"
  }
}

فایل انتخاب شد (در حال آپلود)

{
  "action": "upload_file",
  "success": true,
  "status": "picked",
  "data": {
    "files": [
      {
        "name": "IMG_1234.jpg",
        "size": 842310,
        "mimeType": "image/jpeg"
      }
    ]
  }
}

پیشرفت آپلود (فقط حالت upload)

{
  "action": "upload_file",
  "success": true,
  "status": "progress",
  "data": {
    "progress": 64,
    "sent": 539000,
    "total": 842310,
    "fileName": "IMG_1234.jpg"
  }
}

اتمام موفق (حالت upload)

{
  "action": "upload_file",
  "success": true,
  "status": "completed",
  "data": {
    "files": [
      {
        "url": "https://cdn.example.com/media/IMG_1234.jpg",
        "name": "IMG_1234.jpg",
        "size": 842310,
        "mimeType": "image/jpeg"
      }
    ]
  }
}

اتمام موفق (حالت base64)

{
  "action": "upload_file",
  "success": true,
  "status": "completed",
  "data": {
    "files": [
      {
        "base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
        "name": "avatar.jpg",
        "size": 102400,
        "mimeType": "image/jpeg"
      }
    ]
  }
}

کنسل شدن توسط کاربر

{
  "action": "upload_file",
  "success": false,
  "status": "cancelled",
  "message": "User cancelled file selection"
}

خطا (حجم زیاد / عدم دسترسی / خطای شبکه)

{
  "action": "upload_file",
  "success": false,
  "status": "failed",
  "message": "File exceeds maxBytes (5242880)"
}

مدیریت Response در Web

وب نباید window.onFlutterResponse را بازنویسی کند؛ در این پروژه index.html یک registry به‌اسم window.addFlutterResponseListener تعریف کرده که پاسخ‌ها را به همهٔ لیسنرها فوروارد می‌کند. پس Web باید فقط لیسنر ثبت کند:

const unsubscribe = window.addFlutterResponseListener?.((response) => {
  if (response.action !== 'upload_file') return;

  switch (response.status) {
    case 'picking':
      // UI را به حالت «در حال انتخاب» ببرید (loading روی دکمه آپلود)
      break;
    case 'picked':
      // فایل انتخاب شد؛ نام/حجم را نمایش دهید. در حالت upload آپلود شروع می‌شود.
      break;
    case 'progress':
      // response.data.progress عدد 0 تا 100 است؛ نوار پیشرفت را آپدیت کنید
      break;
    case 'completed': {
      // response.data.files آرایه‌ای از فایل‌هاست
      const file = response.data.files[0];
      const fileUrl = file.url ?? file.base64;
      // اگر returnAs=upload بود file.url یک URL سرور است؛
      // اگر returnAs=base64 بود file.base64 یک data URL است.
      saveField(fileUrl);
      break;
    }
    case 'cancelled':
      // کاربر انتخاب را لغو کرد؛ UI را به حالت اولیه برگردانید
      break;
    case 'failed':
      // response.message را نمایش دهید
      showError(response.message);
      break;
  }
});

// برای unsubscribe (مثلاً در useEffect پاک‌سازی):
// unsubscribe?.();

تبدیل data URL به File در حالت base64 (اختیاری)

اگر Web در حالت base64 همان رفتار سرویس uploadMedia را می‌خواهد، می‌تواند data URL را به File تبدیل کند و به سرویس موجود بدهد:

async function dataUrlToFile(dataUrl, fileName) {
  const res = await fetch(dataUrl);
  const blob = await res.blob();
  return new File([blob], fileName, { type: blob.type });
}

// در listener حالت completed با returnAs=base64:
const file = await dataUrlToFile(response.data.files[0].base64, response.data.files[0].name);
const result = await uploadMedia(file); // سرویس موجود در src/services/upload.ts

نکات مهم برای Web

  • داخل WebView واقعی همیشه window.HabibApp وجود دارد؛ برای تشخیص محیط از helper موجود isInFlutterWebView() در src/services/auth-token.ts استفاده کنید و در خارج از WebView به <input type="file"> به‌عنوان fallback روی بیایید.
  • برای فایل‌های بزرگ (ویدیو/صوت) حتماً از returnAs: 'upload' استفاده کنید تا داده از کانال JS عبور نکند؛ base64 فقط برای تصویر کوچک مناسب است.
  • uploadUrl باید همان endpointی باشد که بک‌اند فایل را قبول می‌کند (در این پروژه /hussainya/dashboard/upload-media/ با فیلد file — مطابق src/services/upload.ts).
  • اگر endpoint نیاز به توکن دارد، یا headers را ارسال کنید یا به Flutter اجازه دهید توکن/کوکی فعلی را خودش تزریق کند.
  • maxBytes را همیشه بفرستید تا Flutter قبل از شروع آپلود فایل بزرگ را رد کند و پهنای باند هدر نرود.
  • وب فقط درخواست می‌فرستد؛ انتخاب فایل، permission دوربین/گالری، resize تصویر، multipart upload، progress و مدیریت platform بر عهده Flutter است.
  • اگر چند فایل با multiple: true انتخاب شود، response.data.files آرایه‌ای با چند عضو خواهد بود و برای هر فایل یک progress جدا ارسال می‌شود (با fileName متمایز).

پیشنهاد پیاده‌سازی سمت Flutter

الگوی dispatch و response دقیقاً مثل get_location در lib/features/web_app/web_app_screen.dart است:

return switch (data['action'].toString().toLowerCase()) {
  // ... اکشن‌های موجود
  'upload_file' => _handleUploadFile(data['data']),
  _ => null,
};

الگوی هندلر (به‌عنوان مرجع، مطابق الگوی _handleGetLocation و upload_song_page.dart):

void _handleUploadFile(dynamic payload) async {
  final data = Map<String, dynamic>.from(payload as Map);
  final mediaType = data['mediaType']?.toString() ?? 'file';
  final source = data['source']?.toString() ?? 'gallery';
  final returnAs = data['returnAs']?.toString() ?? 'upload';

  try {
    _sendResponseToWeb({'action': 'upload_file', 'success': true, 'status': 'picking'});

    // 1) انتخاب فایل با image_picker / file_picker طبق mediaType
    final List<File> files = await _pickFiles(mediaType, source, data);

    _sendResponseToWeb({
      'action': 'upload_file',
      'success': true,
      'status': 'picked',
      'data': {'files': files.map(_fileInfo).toList()},
    });

    // 2) اگر حالت upload بود، با dio/multipart به uploadUrl بفرست
    if (returnAs == 'upload') {
      final uploadUrl = data['uploadUrl'].toString();
      final results = await _uploadFilesWithProgress(
        files,
        uploadUrl: uploadUrl,
        fieldName: data['fieldName']?.toString() ?? 'file',
        onProgress: (p, sent, total, name) {
          _sendResponseToWeb({
            'action': 'upload_file',
            'success': true,
            'status': 'progress',
            'data': {'progress': p, 'sent': sent, 'total': total, 'fileName': name},
          });
        },
      );

      _sendResponseToWeb({
        'action': 'upload_file',
        'success': true,
        'status': 'completed',
        'data': {'files': results},
      });
    } else {
      // حالت base64 (فقط image)
      _sendResponseToWeb({
        'action': 'upload_file',
        'success': true,
        'status': 'completed',
        'data': {'files': await _filesToBase64(files)},
      });
    }
  } catch (error) {
    final cancelled = error is _PickerCancelledException;
    _sendResponseToWeb({
      'action': 'upload_file',
      'success': false,
      'status': cancelled ? 'cancelled' : 'failed',
      'message': cancelled ? 'User cancelled file selection' : error.toString(),
    });
  }
}

وابستگی‌ها

image_picker و file_picker از قبل در pubspec.yaml موجودند و نیازی به افزودن پکیج جدید نیست؛ برای آپلود در سمت Flutter از dio (موجود) با MultipartFile و onSendProgress استفاده شود.


جمع‌بندی actionها

Action کاربرد
download_file دانلود native هر نوع فایل
upload_file انتخاب فایل/تصویر/ویدیو از دستگاه و آپلود به سرور (یا بازگرداندن base64)
copy_to_clipboard کپی متن در clipboard دستگاه
open_external_url باز کردن URL در مرورگر یا اپ خارجی