flutter build web vert. Un seul helper, ImageCompressor, appelé par les DEUX chemins d'upload de resources_screen — c'est le motif qui a fait diverger Create et Upload côté serveur deux fois de suite : sur StoragePath en C1, sur le pré-vol de quota en C3. La compression se fait AVANT resourceCreate, pas avant l'upload. Le contrôle de quota livré en C3 porte sur sizeBytes à la création : le faire après aurait fait décider le serveur sur la taille du fichier d'origine, et compté au quota une image qui n'existe pas. Un quota qui compte 12x trop se remplit 12x trop vite. Deux écarts assumés par rapport à l'énoncé « 2560 px / JPEG q82 » : - Un PNG à canal alpha reste un PNG. Le passer en JPEG remplirait la transparence de noir, ce qui abîmerait logos et filigranes. Le redimensionnement s'applique dans les deux cas ; seul l'encodage diffère, et le type MIME suit — sinon Firebase servirait un JPEG étiqueté PNG. - Si le ré-encodage produit un fichier plus gros que l'original — ce qui arrive sur une petite image déjà bien encodée — l'original est conservé. Un échec de décodage renvoie aussi l'original plutôt que de bloquer l'upload. Défaut préexistant corrigé au passage : le second chemin d'upload ne renseignait sizeBytes ni avant ni après. Toutes les ressources créées par ce chemin comptent donc 0 octet au quota sur l'existant — même famille que ce que C1 et C3 ont trouvé côté serveur, et un argument de plus pour le backfill C2. image 4.2.0 déclarée explicitement : elle était déjà présente en transitif, dépendre d'un hasard de résolution pour une fonction utilisateur n'est pas tenable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
362 lines
15 KiB
Dart
362 lines
15 KiB
Dart
import 'dart:io';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:firebase_storage/firebase_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:manager_app/Components/common_loader.dart';
|
|
import 'package:manager_app/Helpers/ImageCompressor.dart';
|
|
import 'package:manager_app/Components/message_notification.dart';
|
|
import 'package:manager_app/l10n/app_localizations.dart';
|
|
import 'package:manager_app/Models/managerContext.dart';
|
|
import 'package:manager_app/Screens/Resources/new_resource_popup.dart';
|
|
import 'package:manager_app/Screens/Resources/resource_body_grid.dart';
|
|
import 'package:manager_app/Screens/Resources/show_resource_popup.dart';
|
|
import 'package:manager_app/app_context.dart';
|
|
import 'package:manager_app/constants.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:path/path.dart' as Path;
|
|
|
|
class ResourcesScreen extends StatefulWidget {
|
|
final Function? onGetResult; //return ResourceDTO
|
|
final bool isImage;
|
|
final bool isAddButton;
|
|
final bool isSelect;
|
|
final bool isRemoveButton;
|
|
final bool isFilter;
|
|
final List<ResourceType> resourceTypes;
|
|
const ResourcesScreen({
|
|
Key? key,
|
|
this.isImage = false,
|
|
this.onGetResult,
|
|
this.isAddButton = true,
|
|
this.isRemoveButton = false,
|
|
this.isSelect = false,
|
|
required this.resourceTypes,
|
|
this.isFilter = true
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
_ResourcesScreenState createState() => _ResourcesScreenState();
|
|
}
|
|
|
|
class _ResourcesScreenState extends State<ResourcesScreen> {
|
|
String? filter;
|
|
bool? isLoading;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final appContext = Provider.of<AppContext>(context);
|
|
Size size = MediaQuery.of(context).size;
|
|
|
|
return FutureBuilder(
|
|
future: getResources(widget.onGetResult, widget.isImage, appContext, widget.resourceTypes),
|
|
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.done) {
|
|
if(snapshot.data != null) {
|
|
List<ResourceDTO> tempOutput = [];
|
|
if(widget.isRemoveButton) {
|
|
tempOutput.add(ResourceDTO());
|
|
tempOutput.addAll(new List<ResourceDTO>.from(snapshot.data));
|
|
} else {
|
|
tempOutput = new List<ResourceDTO>.from(snapshot.data);
|
|
}
|
|
|
|
return ResourceBodyGrid(
|
|
resources: tempOutput,
|
|
resourceTypesIn: widget.isImage ? resource_types.where((rt) => rt.type == ResourceType.Image || rt.type == ResourceType.ImageUrl).map((rt) => rt.type).toList() : widget.resourceTypes,
|
|
isAddButton: widget.isAddButton,
|
|
isSelectModal: widget.isSelect,
|
|
onSelect: (value) async {
|
|
if(widget.isSelect) {
|
|
if (value.id == "-1" && widget.isAddButton) {
|
|
List<dynamic>? result = await showNewResource(appContext, context);
|
|
if (result != null)
|
|
{
|
|
await create(result[0], result[1], result[2], appContext, context);
|
|
Future.delayed(Duration(seconds: 2), () {
|
|
setState(() {});
|
|
});
|
|
// For refresh
|
|
}
|
|
} else {
|
|
// Result for select modal
|
|
widget.onGetResult!(value);
|
|
}
|
|
} else {
|
|
if (widget.onGetResult == null) {
|
|
// Main screen
|
|
if (value.id == null) {
|
|
List<dynamic>? result = await showNewResource(appContext, context);
|
|
if (result != null)
|
|
{
|
|
await create(result[0], result[1], result[2], appContext, context);
|
|
Future.delayed(Duration(seconds: 2), () {
|
|
setState(() {});
|
|
});
|
|
// For refresh
|
|
}
|
|
} else {
|
|
var result = await showResource(value, appContext, context, size);
|
|
if(result != null) {
|
|
// Update resource
|
|
ManagerAppContext managerAppContext = appContext.getContext() as ManagerAppContext;
|
|
try{
|
|
var resourceUpdated = managerAppContext.clientAPI!.resourceApi!.resourceUpdate(result);
|
|
setState(() {
|
|
// refresh ui
|
|
showNotification(kSuccess, kWhite, AppLocalizations.of(context)!.resourceUpdatedSuccess, context, null);
|
|
});
|
|
} catch (e) {
|
|
print("Error during updating resource");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Result for select modal
|
|
widget.onGetResult!(value);
|
|
}
|
|
}
|
|
});//bodyGrid(tempOutput, size, appContext);
|
|
} else {
|
|
return Text(AppLocalizations.of(context)!.noData);
|
|
}
|
|
} else if (snapshot.connectionState == ConnectionState.none) {
|
|
return Text(AppLocalizations.of(context)!.noData);
|
|
} else {
|
|
return Center(
|
|
child: Container(
|
|
height: size.height * 0.2,
|
|
child: CommonLoader()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<List<ResourceDTO>?> getResources(Function? onGetResult, bool isImage, AppContext appContext, List<ResourceType> types) async {
|
|
types = types != null ? types : [];
|
|
List<ResourceDTO>? resources = await (appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceGet(instanceId: (appContext.getContext() as ManagerAppContext).instanceId, types: types);
|
|
if (onGetResult != null && isImage && resources != null) {
|
|
resources = resources.where((element) => element.type == ResourceType.Image || element.type == ResourceType.ImageUrl || element.type == ResourceType.Audio).toList();
|
|
}
|
|
return resources;
|
|
}
|
|
|
|
Future<List<ResourceDTO?>?> create(ResourceDTO resourceDTO, List<File>? files, List<PlatformFile>? filesWeb, AppContext appContext, context) async {
|
|
List<ResourceDTO?> createdResources = [];
|
|
ManagerAppContext managerAppContext = appContext.getContext();
|
|
int index = 0;
|
|
|
|
if(filesWeb != null) {
|
|
// Vérification quota stockage avant upload
|
|
if ((managerAppContext.instanceDTO?.storageQuotaBytes ?? 0) > 0) {
|
|
try {
|
|
final quotaUri = Uri.parse('${managerAppContext.host}/api/Instance/${managerAppContext.instanceId}/quota');
|
|
final quotaResponse = await http.get(quotaUri, headers: {'Authorization': 'Bearer ${managerAppContext.accessToken}'});
|
|
if (quotaResponse.statusCode == 200) {
|
|
final quotaData = jsonDecode(quotaResponse.body);
|
|
final storageUsed = (quotaData['storageUsedBytes'] as num).toInt();
|
|
final storageQuota = (quotaData['storageQuotaBytes'] as num).toInt();
|
|
final incomingBytes = filesWeb.fold<int>(0, (sum, f) => sum + (f.size));
|
|
if (storageUsed + incomingBytes > storageQuota) {
|
|
showNotification(kError, kWhite, 'Quota de stockage dépassé', context, null);
|
|
return null;
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
for (PlatformFile platformFile in filesWeb) {
|
|
var mimeType = "";
|
|
ResourceDTO resourceDTO = new ResourceDTO(label: platformFile.name);
|
|
switch(platformFile.extension) {
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
case 'png':
|
|
case 'gif':
|
|
resourceDTO.type = ResourceType.Image;
|
|
mimeType = 'image/'+platformFile.extension!;
|
|
break;
|
|
case 'mp3':
|
|
resourceDTO.type = ResourceType.Audio;
|
|
mimeType = 'audio/'+platformFile.extension!;
|
|
break;
|
|
case 'mp4':
|
|
resourceDTO.type = ResourceType.Video;
|
|
mimeType = 'video/'+platformFile.extension!;
|
|
break;
|
|
case 'webm':
|
|
resourceDTO.type = ResourceType.Video;
|
|
mimeType = 'video/'+platformFile.extension!;
|
|
break;
|
|
case 'pdf':
|
|
resourceDTO.type = ResourceType.Pdf;
|
|
mimeType = 'application/'+platformFile.extension!;
|
|
break;
|
|
case 'json':
|
|
resourceDTO.type = ResourceType.Json;
|
|
mimeType = 'application/'+platformFile.extension!;
|
|
break;
|
|
}
|
|
resourceDTO.instanceId = managerAppContext.instanceId;
|
|
try {
|
|
print("Trying to create resource");
|
|
// ADD LOADING
|
|
resourceDTO.dateCreation = DateTime.now();
|
|
// Compresser AVANT resourceCreate : le contrôle de quota du serveur (C3)
|
|
// se fait sur sizeBytes à la création, il doit voir la taille réellement
|
|
// téléversée et non celle du fichier d'origine.
|
|
final compressed = ImageCompressor.compress(platformFile.bytes!, platformFile.extension, mimeType);
|
|
resourceDTO.sizeBytes = compressed.sizeBytes;
|
|
ResourceDTO? newResource = await managerAppContext.clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
|
print("created resource");
|
|
print(newResource);
|
|
if(newResource != null && resourceDTO.type != null) {
|
|
FirebaseStorage storage = FirebaseStorage.instance;
|
|
Reference ref = storage.ref().child('pictures/${appContext.getContext().instanceId}/${Path.basename(newResource.id!.toString())}');
|
|
|
|
var metadata = SettableMetadata(
|
|
// Le MIME suit la compression : un PNG sans alpha ressort en JPEG.
|
|
contentType: compressed.mimeType,
|
|
);
|
|
|
|
UploadTask uploadTask = ref.putData(compressed.bytes, metadata);
|
|
uploadTask.then((res) {
|
|
res.ref.getDownloadURL().then((urlRessource) {
|
|
showNotification(Colors.green, kWhite, AppLocalizations.of(context)!.resourceCreatedSuccess, context, null);
|
|
newResource.url = urlRessource;
|
|
newResource.sizeBytes = compressed.sizeBytes;
|
|
(appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceUpdate(newResource);
|
|
createdResources.add(newResource);
|
|
index++;
|
|
if(index == filesWeb.length) {
|
|
return createdResources;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch(e) {
|
|
print("ERROR creating resource");
|
|
print(e);
|
|
}
|
|
}
|
|
} else {
|
|
// online resource
|
|
print("Trying to create resource");
|
|
resourceDTO.dateCreation = DateTime.now();
|
|
resourceDTO.label = "Resource en ligne";
|
|
resourceDTO.instanceId = managerAppContext.instanceId;
|
|
ResourceDTO? newResource = await managerAppContext.clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
|
print("created resource");
|
|
createdResources.add(newResource);
|
|
return createdResources;
|
|
}
|
|
|
|
return null;
|
|
|
|
/*switch(resourceDTO.type) {
|
|
case ResourceType.Audio:
|
|
case ResourceType.Image:
|
|
case ResourceType.Video:
|
|
|
|
var request = http.MultipartRequest('POST', Uri.parse((appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.apiClient.basePath+"/api/Resource/upload"));
|
|
|
|
if (kIsWeb) {
|
|
for (PlatformFile file in filesWeb!) {
|
|
request.files.add(
|
|
await http.MultipartFile.fromBytes(
|
|
'picture',
|
|
file.bytes!,
|
|
filename: file.name
|
|
)
|
|
);
|
|
}
|
|
} else {
|
|
for (File file in files!) {
|
|
request.files.add(
|
|
await http.MultipartFile(
|
|
'picture',
|
|
file.readAsBytes().asStream(),
|
|
file.lengthSync(),
|
|
filename: file.path.toString().split("/").last
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
ManagerAppContext managerAppContext = appContext.getContext();
|
|
request.headers["authorization"]="Bearer ${managerAppContext.accessToken}";
|
|
request.fields['label'] = resourceDTO.label!;
|
|
request.fields['type'] = resourceDTO.type.toString();
|
|
request.fields['instanceId'] = managerAppContext.instanceId!;
|
|
|
|
var res = await request.send();
|
|
await res.stream.bytesToString();
|
|
|
|
if (res.statusCode == 200) {
|
|
// TODO just create resource ref with download url from followed
|
|
try {
|
|
for (PlatformFile platformFile in filesWeb!) {
|
|
resourceDTO.instanceId = (appContext.getContext() as ManagerAppContext).instanceId;
|
|
ResourceDTO? newResource = await (appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
|
|
|
if(newResource != null) {
|
|
FirebaseStorage storage = FirebaseStorage.instance;
|
|
Reference ref = storage.ref().child('pictures/${appContext.getContext().instanceId}/${Path.basename(newResource.id!.toString())}');
|
|
|
|
final compressed = ImageCompressor.compress(
|
|
platformFile.bytes!,
|
|
platformFile.extension,
|
|
'application/octet-stream',
|
|
);
|
|
|
|
UploadTask uploadTask = ref.putData(
|
|
compressed.bytes,
|
|
SettableMetadata(contentType: compressed.mimeType),
|
|
);
|
|
uploadTask.then((res) {
|
|
res.ref.getDownloadURL().then((urlImage) {
|
|
showNotification(Colors.green, kWhite, 'La ressource a été créée avec succès', context, null);
|
|
newResource.url = urlImage;
|
|
newResource.sizeBytes = compressed.sizeBytes;
|
|
(appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceUpdate(newResource);
|
|
return newResource;
|
|
});
|
|
});
|
|
}
|
|
showNotification(Colors.orange, kWhite, 'Une erreur est survenue lors de la création de la ressource', context, null);
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
print('Error uploading file: $e');
|
|
}
|
|
return null;
|
|
} else {
|
|
showNotification(kPrimaryColor, kWhite, 'Erreur, attention, la taille de la ressource doit être inférieure à 1.5Mb', context, null);
|
|
}
|
|
break;
|
|
case ResourceType.ImageUrl:
|
|
case ResourceType.VideoUrl:
|
|
if (resourceDTO.url != null) {
|
|
// test if Correct url
|
|
bool _validURL = Uri.parse(resourceDTO.url!).isAbsolute;
|
|
if(_validURL) {
|
|
resourceDTO.instanceId = (appContext.getContext() as ManagerAppContext).instanceId;
|
|
ResourceDTO? newResource = await (appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
|
showNotification(Colors.green, kWhite, 'La ressource a été créée avec succès', context, null);
|
|
|
|
return newResource;
|
|
} else {
|
|
showNotification(Colors.orange, kWhite, 'L\'url est invalide', context, null);
|
|
}
|
|
} else {
|
|
showNotification(Colors.orange, kWhite, 'Veuillez remplir le champ URL', context, null);
|
|
}
|
|
break;
|
|
}*/
|
|
} |