Add full app source
Adds the Flutter inventory app source, cleaned up for a shared repo: generic package name, no hardcoded ERP endpoints (moved to gitignored local config), no dead auth code, no debug logging of session data.
This commit is contained in:
155
lib/service/product_image_updater.dart
Normal file
155
lib/service/product_image_updater.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart' as d;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:inventory_app/models/product.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class ProductImageUpdater {
|
||||
final d.Dio dio = d.Dio();
|
||||
final String baseUrl;
|
||||
final box = GetStorage();
|
||||
|
||||
ProductImageUpdater({required this.baseUrl});
|
||||
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
/// Step 1: Pick or take a photo
|
||||
Future<File?> pickPhoto({bool fromCamera = true}) async {
|
||||
final XFile? photo = await _picker.pickImage(
|
||||
source: fromCamera ? ImageSource.camera : ImageSource.gallery,
|
||||
);
|
||||
return photo != null ? File(photo.path) : null;
|
||||
}
|
||||
|
||||
Future<int?> uploadMetaFile(File file) async {
|
||||
final sessionId = box.read('sessionId');
|
||||
if (sessionId == null) {
|
||||
Get.snackbar("Erreur", "Session ID introuvable",backgroundColor: Colors.red);
|
||||
return null;
|
||||
}
|
||||
|
||||
final fileName = file.path.split('/').last;
|
||||
final fileSize = await file.length();
|
||||
|
||||
if (fileSize > 5 * 1024 * 1024) {
|
||||
Get.snackbar("Erreur", "Fichier trop volumineux. Max: 5MB",backgroundColor: Colors.red);
|
||||
return null;
|
||||
}
|
||||
|
||||
final mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
|
||||
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'$baseUrl/ws/files/upload',
|
||||
data: file.openRead(), // ✅ Send binary stream
|
||||
options: d.Options(
|
||||
headers: {
|
||||
'Cookie': 'JSESSIONID=$sessionId',
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-File-Name': fileName,
|
||||
'X-File-Size': fileSize.toString(),
|
||||
'X-File-Type': mimeType,
|
||||
'X-File-Offset': '0',
|
||||
},
|
||||
responseType: d.ResponseType.json,
|
||||
),
|
||||
);
|
||||
|
||||
print('📦 StatusCode: ${response.statusCode}');
|
||||
print('📦 Response: ${response.data}');
|
||||
|
||||
if (response.statusCode == 200 && response != null) {
|
||||
final int id = response.data['id'];
|
||||
print('✅ File uploaded, MetaFile ID: $id');
|
||||
return id;
|
||||
} else {
|
||||
print('❌ Upload failed: ${response.data}');
|
||||
}
|
||||
} on d.DioException catch (e) {
|
||||
print('❌ DioException: ${e.message}');
|
||||
if (e.response != null) {
|
||||
print('📄 Response data: ${e.response?.data}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Unexpected error: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Step 3: Update the product with the image ID
|
||||
Future<bool> updateProductImage({
|
||||
required Product product,
|
||||
required int imageId,
|
||||
}) async {
|
||||
final sessionId = box.read('sessionId');
|
||||
if (sessionId == null) {
|
||||
Get.snackbar("Erreur", "Session ID introuvable",backgroundColor: Colors.red);
|
||||
return false;
|
||||
}
|
||||
|
||||
final payload = {
|
||||
"data": {
|
||||
"id": product.id,
|
||||
"version": product.version,
|
||||
"picture": {"id": imageId},
|
||||
},
|
||||
};
|
||||
|
||||
print('✅ payload $payload');
|
||||
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'$baseUrl/ws/rest/com.axelor.apps.base.db.Product/${product.id}',
|
||||
data: jsonEncode(payload),
|
||||
options: d.Options(
|
||||
headers: {
|
||||
'Cookie': 'JSESSIONID=$sessionId',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data['status'] == 0) {
|
||||
print('✅ Product image updated');
|
||||
return true;
|
||||
} else {
|
||||
print('❌ Update failed: ${response.data}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ d.Dio error: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Step 4: Combine all actions in one flow
|
||||
Future<void> updateProductPictureFlow(
|
||||
BuildContext context,
|
||||
Product product,
|
||||
) async {
|
||||
File? file = await pickPhoto();
|
||||
if (file == null) {
|
||||
Get.snackbar("Annulé", "Aucune image sélectionnée",backgroundColor: Colors.red);
|
||||
return;
|
||||
}
|
||||
|
||||
final imageId = await uploadMetaFile(file);
|
||||
print('✅ imageId $imageId');
|
||||
if (imageId == null) {
|
||||
Get.snackbar("Erreur", "Upload échoué",backgroundColor: Colors.red);
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await updateProductImage(
|
||||
product: product,
|
||||
imageId: imageId,
|
||||
);
|
||||
if (success) {
|
||||
Get.snackbar("Succès", "Image du produit mise à jour",backgroundColor: Colors.green);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user