# WebView Utility Actions این سند ساختار actionهای عمومی WebView به Flutter را تعریف می‌کند. این actionها برای کارهایی هستند که بهتر است به‌صورت native توسط Flutter انجام شوند: دانلود فایل، آپلود فایل (انتخاب فایل/تصویر/ویدیو از دستگاه)، کپی متن در clipboard دستگاه، و باز کردن لینک در مرورگر/اپ خارجی کاربر. ## کانال ارتباطی وب باید از کانال فعلی WebView استفاده کند: ```js HabibApp.postMessage(JSON.stringify(message)); ``` Flutter هم مثل سایر actionها پاسخ را از این مسیر به Web برمی‌گرداند: ```js window.onFlutterResponse(response); ``` --- ## ۱. دانلود فایل ### هدف وب نباید خودش فایل را داخل WebView دانلود کند. وقتی کاربر روی دکمه دانلود هر فایلی کلیک کرد، وب باید یک پیام JSON به Flutter بفرستد تا Flutter دانلود native را برای کاربر انجام دهد. ### Action اصلی نام action باید عمومی باشد: ```text download_file ``` از `download_audio` استفاده نشود، چون فقط یک نوع فایل را پوشش می‌دهد. ### پیام ارسالی از Web به Flutter ```json { "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 معتبر | ### مثال‌ها ### دانلود تصویر ```js 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' } })); } ``` ### دانلود ویدیو ```js 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 } } })); } ``` ### دانلود فایل صوتی ```js 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 ```js 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' } })); } ``` ### نمونه استفاده روی دکمه دانلود ```jsx ``` ### شروع دانلود ```json { "action": "download_file", "success": true, "status": "started", "data": { "fileName": "sample.pdf", "fileType": "document", "mimeType": "application/pdf" } } ``` ### پیشرفت دانلود ```json { "action": "download_file", "success": true, "status": "progress", "data": { "progress": 42, "received": 420000, "total": 1000000, "fileName": "sample.pdf" } } ``` ### اتمام دانلود ```json { "action": "download_file", "success": true, "status": "completed", "data": { "fileName": "sample.pdf", "fileType": "document", "mimeType": "application/pdf" } } ``` ### خطا در دانلود ```json { "action": "download_file", "success": false, "status": "failed", "message": "Invalid download url" } ``` ### مدیریت Response در Web ```js 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: ```dart case 'download_file': return _handleDownloadFile(data); ``` در صورت نیاز موقت به backward compatibility: ```dart 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 اصلی ```text copy_to_clipboard ``` ### پیام ارسالی از Web به Flutter ```json { "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 ```js function copyText(text) { window.HabibApp?.postMessage(JSON.stringify({ action: 'copy_to_clipboard', data: { text, label: 'share_text', showToast: true } })); } ``` ### نمونه استفاده روی دکمه ```jsx ``` ### Response موفق ```json { "action": "copy_to_clipboard", "success": true, "status": "completed", "data": { "label": "share_text" } } ``` ### Response خطا ```json { "action": "copy_to_clipboard", "success": false, "status": "failed", "message": "Text is empty" } ``` ### مدیریت Response در Web ```js window.onFlutterResponse = function (response) { if (response.action !== 'copy_to_clipboard') return; if (response.success) { // UI کپی موفق را نمایش دهید } else { // پیام خطا نمایش دهید } }; ``` ### پیشنهاد fallback سمت Web اگر Web خارج از اپ هم اجرا می‌شود، می‌تواند helper عمومی داشته باشد: ```js 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 اصلی ```text open_external_url ``` ### پیام ارسالی از Web به Flutter ```json { "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 برای لینک وب ```js function openInBrowser(url) { window.HabibApp?.postMessage(JSON.stringify({ action: 'open_external_url', data: { url, mode: 'externalApplication', showErrorToast: true } })); } ``` ### نمونه استفاده روی دکمه ```jsx ``` ### مثال برای واتساپ ```js function openWhatsapp(phone) { window.HabibApp?.postMessage(JSON.stringify({ action: 'open_external_url', data: { url: `https://wa.me/${phone}`, mode: 'externalApplication', title: 'whatsapp_contact' } })); } ``` ### مثال برای تماس تلفنی ```js function callPhone(phone) { window.HabibApp?.postMessage(JSON.stringify({ action: 'open_external_url', data: { url: `tel:${phone}`, mode: 'externalApplication', title: 'phone_call' } })); } ``` ### Response موفق ```json { "action": "open_external_url", "success": true, "status": "opened", "data": { "url": "https://example.com/page" } } ``` ### Response خطا ```json { "action": "open_external_url", "success": false, "status": "failed", "message": "Cannot launch url" } ``` ### مدیریت Response در Web ```js 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 دکمه‌ای مثل «آپلود» / «انتخاب تصویر» / «انتخاب ویدیو» کلیک می‌شود، وب نباید از `` خود مرورگر استفاده کند چون داخل 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 اصلی ```text upload_file ``` از نام‌های خاص مثل `upload_image` یا `pick_video` استفاده نشود؛ `upload_file` با فیلد `mediaType` همهٔ نوع‌ها را پوشش می‌دهد. ### پیام ارسالی از Web به Flutter ```json { "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) ```js 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' } })); } ``` ### انتخاب از دوربین (سلفی/گواهی) ```js 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' } })); } ``` ### انتخاب ویدیو و آپلود ```js 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) ```js 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) ```js function pickAvatarAsBase64() { window.HabibApp?.postMessage(JSON.stringify({ action: 'upload_file', data: { mediaType: 'image', source: 'gallery', returnAs: 'base64', maxBytes: 1048576, maxWidth: 256, imageQuality: 80 } })); } ``` ### نمونه استفاده روی دکمه آپلود ```jsx ``` ### جریان Response در حالت `upload`، Flutter چندین response پشت سر هم می‌فرستد (درست مثل `download_file`): ### شروع انتخاب ```json { "action": "upload_file", "success": true, "status": "picking", "data": { "mediaType": "image", "source": "gallery" } } ``` ### فایل انتخاب شد (در حال آپلود) ```json { "action": "upload_file", "success": true, "status": "picked", "data": { "files": [ { "name": "IMG_1234.jpg", "size": 842310, "mimeType": "image/jpeg" } ] } } ``` ### پیشرفت آپلود (فقط حالت `upload`) ```json { "action": "upload_file", "success": true, "status": "progress", "data": { "progress": 64, "sent": 539000, "total": 842310, "fileName": "IMG_1234.jpg" } } ``` ### اتمام موفق (حالت `upload`) ```json { "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`) ```json { "action": "upload_file", "success": true, "status": "completed", "data": { "files": [ { "base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...", "name": "avatar.jpg", "size": 102400, "mimeType": "image/jpeg" } ] } } ``` ### کنسل شدن توسط کاربر ```json { "action": "upload_file", "success": false, "status": "cancelled", "message": "User cancelled file selection" } ``` ### خطا (حجم زیاد / عدم دسترسی / خطای شبکه) ```json { "action": "upload_file", "success": false, "status": "failed", "message": "File exceeds maxBytes (5242880)" } ``` ### مدیریت Response در Web وب نباید `window.onFlutterResponse` را بازنویسی کند؛ در این پروژه `index.html` یک registry به‌اسم `window.addFlutterResponseListener` تعریف کرده که پاسخ‌ها را به همهٔ لیسنرها فوروارد می‌کند. پس Web باید فقط لیسنر ثبت کند: ```js 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` تبدیل کند و به سرویس موجود بدهد: ```js 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 به `` به‌عنوان 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` است: ```dart return switch (data['action'].toString().toLowerCase()) { // ... اکشن‌های موجود 'upload_file' => _handleUploadFile(data['data']), _ => null, }; ``` الگوی هندلر (به‌عنوان مرجع، مطابق الگوی `_handleGetLocation` و `upload_song_page.dart`): ```dart void _handleUploadFile(dynamic payload) async { final data = Map.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 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 در مرورگر یا اپ خارجی |