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 برگرداند.
دو حالت پاسخ وجود دارد:
- حالت آپلود (پیشنهادی، پیشفرض): Flutter فایل را انتخاب کرده و خودش با multipart POST به
uploadUrlمیفرستد و فقط URL نهاییِ سرور را به Web برمیگرداند. این حالت برای تصویر، ویدیو و فایلهای بزرگ بهترین گزینه است چون دادهٔ حجیم از کانال JS عبور نمیکند. - حالت
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 در مرورگر یا اپ خارجی |