تخطي إلى المحتوى الرئيسي

دعم React Native

يعمل Dropup في React Native مع المحول الأصلي.

التثبيت

npm install @samithahansaka/dropup

# لاختيار الصور
npx expo install expo-image-picker

# لاختيار المستندات
npx expo install expo-document-picker

# لمعالجة الصور (اختياري)
npx expo install expo-image-manipulator

الإعداد الأساسي

import { useDropup } from '@samithahansaka/dropup';
import { NativeAdapter } from '@samithahansaka/dropup/native';
import * as ImagePicker from 'expo-image-picker';
import { View, Text, Button, Image, FlatList } from 'react-native';

function NativeUploader() {
const { files, actions, state } = useDropup({
// استخدام المحول الأصلي
adapter: NativeAdapter,

upload: {
url: 'https://your-api.com/upload',
},
});

const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
quality: 0.8,
});

if (!result.canceled) {
// تحويل إلى كائنات تشبه File
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'photo.jpg',
type: asset.mimeType || 'image/jpeg',
}));

actions.addFiles(files);
}
};

return (
<View style={styles.container}>
<Button title="اختيار صور" onPress={pickImage} />

<FlatList
data={files}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.fileItem}>
{item.preview && (
<Image source={{ uri: item.preview }} style={styles.preview} />
)}
<Text>{item.name}</Text>
<Text>{item.status}</Text>
{item.status === 'uploading' && (
<Text>{item.progress}%</Text>
)}
</View>
)}
/>

<Button
title="رفع الكل"
onPress={() => actions.upload()}
disabled={state.isUploading}
/>
</View>
);
}

const styles = {
container: {
flex: 1,
padding: 20,
},
fileItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
preview: {
width: 50,
height: 50,
marginRight: 10,
borderRadius: 4,
},
};

تكامل منتقي الصور

الاختيار من المعرض

import * as ImagePicker from 'expo-image-picker';

const pickFromGallery = async () => {
// طلب الإذن
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('الإذن مطلوب');
return;
}

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: true,
quality: 0.8,
selectionLimit: 10,
});

if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || `photo_${Date.now()}.jpg`,
type: asset.mimeType || 'image/jpeg',
size: asset.fileSize,
}));

actions.addFiles(files);
}
};

التقاط صورة

const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
alert('إذن الكاميرا مطلوب');
return;
}

const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
allowsEditing: true,
aspect: [4, 3],
});

if (!result.canceled) {
const asset = result.assets[0];
actions.addFiles([{
uri: asset.uri,
name: `photo_${Date.now()}.jpg`,
type: asset.mimeType || 'image/jpeg',
}]);
}
};

منتقي المستندات

import * as DocumentPicker from 'expo-document-picker';

const pickDocument = async () => {
const result = await DocumentPicker.getDocumentAsync({
type: ['application/pdf', 'application/msword'],
multiple: true,
});

if (result.type === 'success') {
actions.addFiles([{
uri: result.uri,
name: result.name,
type: result.mimeType,
size: result.size,
}]);
}
};

ضغط الصور (أصلي)

import * as ImageManipulator from 'expo-image-manipulator';

async function compressNativeImage(uri: string): Promise<string> {
const result = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1200 } }],
{ compress: 0.7, format: ImageManipulator.SaveFormat.JPEG }
);

return result.uri;
}

// الاستخدام مع Dropup
const { actions } = useDropup({
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
if (file.type.startsWith('image/')) {
const compressedUri = await compressNativeImage(file.uri);
actions.updateFileMeta(file.id, { compressedUri });
}
}
},
});

مثال أصلي كامل

import React from 'react';
import {
View,
Text,
TouchableOpacity,
Image,
FlatList,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { useDropup } from '@samithahansaka/dropup';
import { NativeAdapter } from '@samithahansaka/dropup/native';
import * as ImagePicker from 'expo-image-picker';
import * as DocumentPicker from 'expo-document-picker';

export default function FileUploader() {
const { files, actions, state } = useDropup({
adapter: NativeAdapter,
maxFiles: 10,
upload: {
url: 'https://your-api.com/upload',
headers: {
'Authorization': 'Bearer token',
},
},
onUploadComplete: (file) => {
console.log('تم الرفع:', file.uploadedUrl);
},
onUploadError: (file, error) => {
console.error('فشل:', error.message);
},
});

const pickImages = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') return;

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
});

if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'image.jpg',
type: asset.mimeType || 'image/jpeg',
}));
actions.addFiles(files);
}
};

const pickDocuments = async () => {
const result = await DocumentPicker.getDocumentAsync({
multiple: true,
});

if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.name,
type: asset.mimeType,
size: asset.size,
}));
actions.addFiles(files);
}
};

const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') return;

const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
});

if (!result.canceled) {
const asset = result.assets[0];
actions.addFiles([{
uri: asset.uri,
name: `photo_${Date.now()}.jpg`,
type: 'image/jpeg',
}]);
}
};

const renderFile = ({ item }) => (
<View style={styles.fileItem}>
{item.preview && (
<Image source={{ uri: item.preview }} style={styles.thumbnail} />
)}
<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.fileStatus}>
{item.status === 'uploading'
? `جاري الرفع ${item.progress}%`
: item.status}
</Text>
</View>
<TouchableOpacity
onPress={() => actions.remove(item.id)}
style={styles.removeBtn}
>
<Text style={styles.removeText}>×</Text>
</TouchableOpacity>
</View>
);

return (
<View style={styles.container}>
{/* أزرار الإجراءات */}
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.button} onPress={pickImages}>
<Text style={styles.buttonText}>المعرض</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={takePhoto}>
<Text style={styles.buttonText}>الكاميرا</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={pickDocuments}>
<Text style={styles.buttonText}>الملفات</Text>
</TouchableOpacity>
</View>

{/* قائمة الملفات */}
<FlatList
data={files}
keyExtractor={item => item.id}
renderItem={renderFile}
style={styles.list}
ListEmptyComponent={
<Text style={styles.emptyText}>لم يتم اختيار ملفات</Text>
}
/>

{/* زر الرفع */}
<TouchableOpacity
style={[styles.uploadBtn, state.isUploading && styles.uploadBtnDisabled]}
onPress={() => actions.upload()}
disabled={state.isUploading || files.length === 0}
>
{state.isUploading ? (
<View style={styles.uploadingRow}>
<ActivityIndicator color="white" />
<Text style={styles.uploadBtnText}>
جاري الرفع {state.progress}%
</Text>
</View>
) : (
<Text style={styles.uploadBtnText}>
رفع {files.length} ملف(ات)
</Text>
)}
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
buttonRow: {
flexDirection: 'row',
gap: 12,
marginBottom: 16,
},
button: {
flex: 1,
backgroundColor: '#f0f0f0',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
fontWeight: '600',
},
list: {
flex: 1,
},
fileItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
thumbnail: {
width: 50,
height: 50,
borderRadius: 4,
marginRight: 12,
},
fileInfo: {
flex: 1,
},
fileName: {
fontWeight: '500',
},
fileStatus: {
color: '#666',
fontSize: 12,
marginTop: 2,
},
removeBtn: {
padding: 8,
},
removeText: {
fontSize: 24,
color: '#999',
},
emptyText: {
textAlign: 'center',
color: '#999',
marginTop: 40,
},
uploadBtn: {
backgroundColor: '#2196f3',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 16,
},
uploadBtnDisabled: {
backgroundColor: '#ccc',
},
uploadBtnText: {
color: 'white',
fontWeight: '600',
fontSize: 16,
},
uploadingRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
});

اختلافات المنصات

الميزةالويبReact Native
السحب والإفلاتنعملا
منتقي الملفاتأصليexpo-image-picker
روابط المعاينةObject URLsURIs الملف
الوصول للملفكائن Fileمبني على URI

Expo مقابل React Native البحت

Expo (مُدار)

جميع الأمثلة أعلاه تعمل مع سير عمل Expo المُدار.

React Native البحت

لـ React Native البحت، ثبّت الوحدات الأصلية:

# بدلاً من حزم expo
npm install react-native-image-picker
npm install react-native-document-picker

# الربط إذا لزم الأمر (إصدارات RN القديمة)
npx react-native link

ثم استخدم APIs المقابلة من تلك الحزم.