asset images upload functionality added for ai training.

ui_ux_rollout_merge
Sikander Saleem 3 weeks ago
parent dbc53d62f8
commit fbc14318e5

@ -0,0 +1,13 @@
<svg width="130" height="150" viewBox="0 0 130 150" xmlns="http://www.w3.org/2000/svg">
<!-- front face -->
<rect x="30" y="46" width="65" height="48" rx="4" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- screen indicator -->
<rect x="40" y="56" width="26" height="16" rx="2" fill="none" stroke="#3D3D3A" stroke-width="1.2"/>
<!-- bottom face (highlighted) -->
<polygon points="30,94 50,114 115,114 95,94" fill="#F5C4B3" stroke="#993C1D" stroke-width="2"/>
<!-- right side face -->
<polygon points="95,46 115,26 115,74 95,94" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- arrow pointing up into bottom face -->
<line x1="73" y1="142" x2="73" y2="118" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="73,112 66,124 80,124" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 808 B

@ -0,0 +1,13 @@
<svg width="150" height="150" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
<!-- front face (highlighted, left side facing camera) -->
<rect x="32" y="52" width="56" height="58" rx="4" fill="#9FE1CB" stroke="#0F6E56" stroke-width="2"/>
<!-- screen indicator -->
<rect x="42" y="62" width="24" height="15" rx="2" fill="none" stroke="#0F6E56" stroke-width="1.2"/>
<!-- top face -->
<polygon points="32,52 52,32 118,32 88,52" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- right side face -->
<polygon points="88,52 118,32 118,90 88,110" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- arrow pointing right onto left face -->
<line x1="5" y1="81" x2="28" y2="81" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="34,81 22,74 22,88" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

@ -0,0 +1,13 @@
<svg width="150" height="150" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
<!-- front-left face -->
<polygon points="28,52 58,32 58,90 28,110" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- top face -->
<polygon points="28,52 58,32 95,32 65,52" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- right face highlighted, facing camera -->
<polygon points="65,52 95,32 95,90 65,110" fill="#FAC775" stroke="#854F0B" stroke-width="2"/>
<!-- screen indicator on front-left face for context -->
<rect x="34" y="60" width="16" height="14" rx="2" fill="none" stroke="#3D3D3A" stroke-width="1"/>
<!-- arrow pointing left onto right face -->
<line x1="145" y1="81" x2="122" y2="81" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="116,81 128,74 128,88" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 837 B

@ -0,0 +1,13 @@
<svg width="130" height="150" viewBox="0 0 130 150" xmlns="http://www.w3.org/2000/svg">
<!-- front face -->
<rect x="30" y="68" width="65" height="48" rx="4" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- screen indicator -->
<rect x="40" y="78" width="26" height="16" rx="2" fill="none" stroke="#3D3D3A" stroke-width="1.2"/>
<!-- top face (highlighted) -->
<polygon points="30,68 50,48 115,48 95,68" fill="#B5D4F4" stroke="#185FA5" stroke-width="2"/>
<!-- right side face -->
<polygon points="95,68 115,48 115,96 95,116" fill="none" stroke="#3D3D3A" stroke-width="2"/>
<!-- arrow pointing down onto top face -->
<line x1="73" y1="8" x2="73" y2="38" stroke="#3D3D3A" stroke-width="3" stroke-linecap="round"/>
<polygon points="73,44 66,32 80,32" fill="#3D3D3A"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

@ -3,13 +3,13 @@ class URLs {
static const String appReleaseBuildNumber = "44"; static const String appReleaseBuildNumber = "44";
static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsm.hmg.com"; // production url
// static const host1 = "https://atomsmdev.hmg.com"; // local DEV url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
// static const host1 = "https://atomsmuat.hmg.com"; // local UAT url static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static final String _baseUrl = "$_host/mobile"; // host local UAT static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM
// static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module
// static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation
// static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module

@ -232,11 +232,21 @@ class AssetProvider extends ChangeNotifier {
} }
} }
Future<bool> uploadAssetImages(int assetId, List<GenericAttachmentModel> attachments) async { Future<bool> uploadAssetImages(int assetId, TrainingImageData trainingImageData) async {
late Response response; late Response response;
try { try {
response = List<Future<MultipartFile>> files = [];
await ApiManager.instance.multiPart(URLs.saveAssetImages + "?AssetId=$assetId", body: {}, files: attachments.map((attachment) => MultipartFile.fromPath("Files", attachment.name!)).toList()); files.add(MultipartFile.fromPath("LeftImage", trainingImageData.leftImage!.name!));
files.add(MultipartFile.fromPath("RightImage", trainingImageData.rightImage!.name!));
files.add(MultipartFile.fromPath("UpImage", trainingImageData.upImage!.name!));
files.add(MultipartFile.fromPath("DownImage", trainingImageData.downImage!.name!));
for (var i = 0; i < (trainingImageData.additionalImages?.length ?? 0); i++) {
files.add(
MultipartFile.fromPath('AdditionalImage', trainingImageData.additionalImages![i].name!),
);
}
response = await ApiManager.instance.multiPart(URLs.saveAssetImages + "?AssetId=$assetId", body: {}, files: files);
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received // client's request was successfully received
// Map<String, dynamic> assetData = json.decode(response.body)["data"]; // Map<String, dynamic> assetData = json.decode(response.body)["data"];

@ -65,6 +65,7 @@ class Asset {
this.comment, this.comment,
this.tagCode, this.tagCode,
this.assetPhoto, this.assetPhoto,
this.hasTrainingImageData,
}); });
Asset.fromJson(dynamic json) { Asset.fromJson(dynamic json) {
@ -122,6 +123,7 @@ class Asset {
comment = json['comment']; comment = json['comment'];
tagCode = json['tagCode']; tagCode = json['tagCode'];
assetPhoto = json['assetPhoto']; assetPhoto = json['assetPhoto'];
hasTrainingImageData = json['hasTrainingImageData'];
} }
num? id; // Now nullable num? id; // Now nullable
@ -177,6 +179,7 @@ class Asset {
String? comment; // Now nullable String? comment; // Now nullable
String? tagCode; // Now nullable String? tagCode; // Now nullable
String? assetPhoto; String? assetPhoto;
bool? hasTrainingImageData;
Asset copyWith({ Asset copyWith({
num? id, num? id,
@ -232,6 +235,7 @@ class Asset {
String? comment, String? comment,
String? tagCode, String? tagCode,
String? assetPhoto, String? assetPhoto,
bool? hasTrainingImageData,
}) => }) =>
Asset( Asset(
id: id ?? this.id, id: id ?? this.id,
@ -287,6 +291,7 @@ class Asset {
comment: comment ?? this.comment, comment: comment ?? this.comment,
tagCode: tagCode ?? this.tagCode, tagCode: tagCode ?? this.tagCode,
assetPhoto: assetPhoto ?? this.assetPhoto, assetPhoto: assetPhoto ?? this.assetPhoto,
hasTrainingImageData: hasTrainingImageData ?? this.hasTrainingImageData,
); );
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -381,6 +386,7 @@ class Asset {
map['comment'] = comment; map['comment'] = comment;
map['tagCode'] = tagCode; map['tagCode'] = tagCode;
map['assetPhoto'] = assetPhoto; map['assetPhoto'] = assetPhoto;
map['hasTrainingImageData'] = hasTrainingImageData;
return map; return map;
} }
} }

@ -1,4 +1,5 @@
import 'package:test_sa/models/device/technical_guidance_book.dart'; import 'package:test_sa/models/device/technical_guidance_book.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
class AssetByIdModel { class AssetByIdModel {
@ -63,6 +64,8 @@ class AssetByIdModel {
String? modelPhoto; String? modelPhoto;
String? nextPMDate; String? nextPMDate;
String? lastPMDate; String? lastPMDate;
bool? hasTrainingImageData;
TrainingImageData? trainingImageData;
AssetByIdModel( AssetByIdModel(
{this.multiAssets, {this.multiAssets,
@ -125,9 +128,13 @@ class AssetByIdModel {
this.assetPhoto, this.assetPhoto,
this.modelPhoto, this.modelPhoto,
this.nextPMDate, this.nextPMDate,
this.lastPMDate}); this.lastPMDate,
this.trainingImageData,
this.hasTrainingImageData});
AssetByIdModel.fromJson(Map<String, dynamic> json) { AssetByIdModel.fromJson(Map<String, dynamic> json) {
print("assetModel!.fromJson:${json['hasTrainingImageData']}");
if (json['multiAssets'] != null) { if (json['multiAssets'] != null) {
multiAssets = <MultiAssets>[]; multiAssets = <MultiAssets>[];
json['multiAssets'].forEach((v) { json['multiAssets'].forEach((v) {
@ -204,6 +211,8 @@ class AssetByIdModel {
modelPhoto = json['modelPhoto']; modelPhoto = json['modelPhoto'];
nextPMDate = json['nextPMDate']; nextPMDate = json['nextPMDate'];
lastPMDate = json['lastPMDate']; lastPMDate = json['lastPMDate'];
hasTrainingImageData = json['hasTrainingImageData'];
trainingImageData = json['trainingImageData'] != null ? TrainingImageData.fromJson(json['trainingImageData']) : null;
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -301,6 +310,8 @@ class AssetByIdModel {
data['modelPhoto'] = modelPhoto; data['modelPhoto'] = modelPhoto;
data['nextPMDate'] = nextPMDate; data['nextPMDate'] = nextPMDate;
data['lastPMDate'] = lastPMDate; data['lastPMDate'] = lastPMDate;
data['hasTrainingImageData'] = hasTrainingImageData;
data['trainingImageData'] = trainingImageData?.toJson();
return data; return data;
} }
} }
@ -789,3 +800,37 @@ class DemoRequest {
return data; return data;
} }
} }
class TrainingImageData {
GenericAttachmentModel? leftImage;
GenericAttachmentModel? rightImage;
GenericAttachmentModel? upImage;
GenericAttachmentModel? downImage;
List<GenericAttachmentModel>? additionalImages;
TrainingImageData({this.leftImage, this.rightImage, this.upImage, this.downImage, this.additionalImages = const []});
TrainingImageData.fromJson(Map<String, dynamic> json) {
leftImage = json['leftImage'] == null ? null : GenericAttachmentModel.fromJson(json['leftImage'], convertToLink: true);
rightImage = json['rightImage'] == null ? null : GenericAttachmentModel.fromJson(json['rightImage'], convertToLink: true);
upImage = json['upImage'] == null ? null : GenericAttachmentModel.fromJson(json['upImage'], convertToLink: true);
downImage = json['downImage'] == null ? null : GenericAttachmentModel.fromJson(json['downImage'], convertToLink: true);
additionalImages = [];
if (json['additionalImages'] != null) {
json['additionalImages'].forEach((v) {
additionalImages!.add(GenericAttachmentModel.fromJson(v, convertToLink: true));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['leftImage'] = leftImage?.toJson();
data['rightImage'] = rightImage?.toJson();
data['upImage'] = upImage?.toJson();
data['downImage'] = downImage?.toJson();
data['additionalImages'] = additionalImages?.map((v) => v.toJson()).toList() ?? [];
return data;
}
}

@ -1,9 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
class GenericAttachmentModel { class GenericAttachmentModel {
GenericAttachmentModel({this.id, this.name, this.originalName, this.createdBy,this.documentTypeId}); GenericAttachmentModel({this.id, this.name, this.originalName, this.createdBy, this.documentTypeId});
int? id; int? id;
String? name; String? name;
@ -11,11 +12,14 @@ class GenericAttachmentModel {
String? originalName; String? originalName;
Lookup? documentTypeId; Lookup? documentTypeId;
GenericAttachmentModel.fromJson(Map<String, dynamic> json) { GenericAttachmentModel.fromJson(Map<String, dynamic> json, {bool convertToLink = false}) {
id = json['id']; id = json['id'];
name = json['name']; name = json['name'] ?? json['imageName'];
createdBy = json['createdBy']; createdBy = json['createdBy'];
originalName = json['originalName']; originalName = json['originalName'] ?? json['imageOriginalName'];
if (convertToLink) {
name = URLs.getFileUrl(name);
}
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {

@ -304,17 +304,20 @@ class WorkOrderAsset {
required this.id, required this.id,
required this.assetNumber, required this.assetNumber,
required this.assetSerialNo, required this.assetSerialNo,
required this.hasTrainingImageData,
}); });
int? id; int? id;
String? assetNumber; String? assetNumber;
String? assetSerialNo; String? assetSerialNo;
bool? hasTrainingImageData;
factory WorkOrderAsset.fromJson(Map<String, dynamic> json) { factory WorkOrderAsset.fromJson(Map<String, dynamic> json) {
return WorkOrderAsset( return WorkOrderAsset(
id: json["id"], id: json["id"],
assetNumber: json["assetNumber"], assetNumber: json["assetNumber"],
assetSerialNo: json["assetSerialNo"], assetSerialNo: json["assetSerialNo"],
hasTrainingImageData: json["hasTrainingImageData"],
); );
} }
@ -322,6 +325,7 @@ class WorkOrderAsset {
"id": id, "id": id,
"assetNumber": assetNumber, "assetNumber": assetNumber,
"assetSerialNo": assetSerialNo, "assetSerialNo": assetSerialNo,
"hasTrainingImageData": hasTrainingImageData,
}; };
} }

@ -206,6 +206,13 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
], ],
), ),
8.height, 8.height,
if (context.userProvider.isEngineer && !(workOrder.asset!.hasTrainingImageData ?? true)) ...[
Text(
'*Images are required for the asset',
style: AppTextStyles.bodyText.copyWith(color: AppColor.red30),
),
4.height,
],
Text( Text(
'${context.translation.assetName}: ${workOrder.assetNdModel!.name?.cleanupWhitespace?.capitalizeFirstOfEach}', '${context.translation.assetName}: ${workOrder.assetNdModel!.name?.cleanupWhitespace?.capitalizeFirstOfEach}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),

@ -201,8 +201,7 @@ class _SearchDevicePageState extends State<SearchDevicePage> {
), ),
), ),
], ],
) )),
),
// Row( // Row(
// mainAxisSize: MainAxisSize.min, // mainAxisSize: MainAxisSize.min,
// children: [ // children: [

@ -117,6 +117,13 @@ class _AssetDetailPageState extends State<AssetDetailPage> {
if (assetModel?.commissioningStatus != null) 8.height, if (assetModel?.commissioningStatus != null) 8.height,
(assetModel?.modelDefinition?.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? "-").heading5(context), (assetModel?.modelDefinition?.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? "-").heading5(context),
8.height, 8.height,
if (context.userProvider.isEngineer && !(assetModel!.hasTrainingImageData ?? true)) ...[
Text(
'*Upload Images for this asset',
style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30),
),
4.height,
],
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -164,12 +171,12 @@ class _AssetDetailPageState extends State<AssetDetailPage> {
], ],
).toShadowContainer(context), ).toShadowContainer(context),
]).expanded, ]).expanded,
if (context.userProvider.isEngineer) if (context.userProvider.isEngineer && !(assetModel!.hasTrainingImageData ?? true))
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
label: "Upload Images (AI Training)", label: "Upload Images",
onPressed: _upload, onPressed: _upload,
), ),
), ),
@ -183,20 +190,7 @@ class _AssetDetailPageState extends State<AssetDetailPage> {
} }
void _upload() async { void _upload() async {
bool reload = await Navigator.push( bool reload = await Navigator.push(context, MaterialPageRoute(builder: (context) => AssetImageUploadPage(assetId: assetId!))) ?? false;
context,
MaterialPageRoute(
builder: (context) => AssetImageUploadPage(
assetId: assetId!,
attachments: assetModel?.assetAttachments
?.map((attachment) => GenericAttachmentModel(
id: attachment.id,
name: attachment.attachmentName,
originalName: attachment.originalName,
))
.toList() ??
[]))) ??
false;
if (reload) { if (reload) {
assetModel = null; assetModel = null;
setState(() {}); setState(() {});

@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/devices_provider.dart'; import 'package:test_sa/controllers/providers/api/devices_provider.dart';
@ -5,7 +8,9 @@ import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/helper/utils.dart';
import 'package:test_sa/models/device/asset_by_id_model.dart';
import 'package:test_sa/models/generic_attachment_model.dart'; import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
@ -15,9 +20,9 @@ import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
class AssetImageUploadPage extends StatefulWidget { class AssetImageUploadPage extends StatefulWidget {
int assetId; int assetId;
List<GenericAttachmentModel> attachments; TrainingImageData? attachments;
AssetImageUploadPage({Key? key, required this.assetId, required this.attachments}) : super(key: key); AssetImageUploadPage({Key? key, required this.assetId, this.attachments}) : super(key: key);
@override @override
_AssetImageUploadPageState createState() { _AssetImageUploadPageState createState() {
@ -26,12 +31,12 @@ class AssetImageUploadPage extends StatefulWidget {
} }
class _AssetImageUploadPageState extends State<AssetImageUploadPage> { class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
List<GenericAttachmentModel> attachments = []; late TrainingImageData trainingImageData;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// attachments = widget.attachments; trainingImageData = widget.attachments ?? TrainingImageData(additionalImages: []);
} }
@override @override
@ -48,16 +53,68 @@ class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
ListView( ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
Text(
"Instructions",
style: AppTextStyles.heading5.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
Text(
'* Please upload all images of the sides of the asset as mentioned below.\n* Images should be clear and high quality.\n* Avoid uploading blurry, low-light or low-resolution images.',
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
GridView(
padding: const EdgeInsets.only(top: 16, bottom: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: (MediaQuery.of(context).size.width < 550) ? 4 : 4, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12),
children: <Widget>[
gridCard(
icon: "asset_image_upload/side_right",
title: "Right Side",
attachment: trainingImageData.rightImage,
onUpdate: (value) {
trainingImageData.rightImage = value;
setState(() {});
},
),
gridCard(
icon: "asset_image_upload/side_left",
title: "Left Side",
attachment: trainingImageData.leftImage,
onUpdate: (value) {
trainingImageData.leftImage = value;
setState(() {});
},
),
gridCard(
icon: "asset_image_upload/side_top",
title: "Top Side",
attachment: trainingImageData.upImage,
onUpdate: (value) {
trainingImageData.upImage = value;
setState(() {});
},
),
gridCard(
icon: "asset_image_upload/side_bottom",
title: "Bottom Side",
attachment: trainingImageData.downImage,
onUpdate: (value) {
trainingImageData.downImage = value;
setState(() {});
},
),
],
),
AttachmentPicker( AttachmentPicker(
label: "Import", label: "Additional Images",
attachment: attachments, attachment: trainingImageData.additionalImages!,
buttonColor: AppColor.black10, buttonColor: AppColor.black10,
onlyImages: true, onlyImages: true,
pickMultiple: true, pickMultiple: true,
showAsListView: true,
buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120), buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120),
onChange: (_attachments) { onChange: (_attachments) {
attachments = _attachments; trainingImageData.additionalImages = _attachments;
setState(() {}); setState(() {});
}, },
).toShadowContainer(context), ).toShadowContainer(context),
@ -67,18 +124,19 @@ class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
disableButton: attachments.length < 4, disableButton: trainingImageData.leftImage == null || trainingImageData.rightImage == null || trainingImageData.upImage == null || trainingImageData.downImage == null,
label: "Upload Images", label: "Upload",
onPressed: () async { onPressed: () async {
bool result = await showDialog( bool result = await showDialog(
context: context, context: context,
builder: (_) => AAlertDialog( builder: (_) => AAlertDialog(
title: context.translation.confirm, title: context.translation.confirm,
content: "For more enhancement, these images will be used to train AI model.\n\nAre you sure you are uploading clear and high quality images?"), content: "I followed all the instructions and uploading the images in high quality, clear and not blurry.",
),
); );
if (result) { if (result) {
Utils.showUploadingDialog(context); Utils.showUploadingDialog(context);
result = await Provider.of<AssetProvider>(context, listen: false).uploadAssetImages(widget.assetId, attachments); result = await Provider.of<AssetProvider>(context, listen: false).uploadAssetImages(widget.assetId, trainingImageData);
Utils.hideLoading(context); Utils.hideLoading(context);
if (result) { if (result) {
Navigator.pop(context, result); Navigator.pop(context, result);
@ -90,4 +148,112 @@ class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
], ],
)); ));
} }
Widget gridCard({
required String icon,
required String title,
required GenericAttachmentModel? attachment,
required Function(GenericAttachmentModel?) onUpdate,
}) {
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Positioned.fill(
child: AttachmentPicker(
label: "",
onlyImages: true,
checkQuality: true,
enabled: attachment == null,
attachment: attachment == null ? [] : [attachment],
buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120),
onChange: (_attachments) {
if (_attachments.isNotEmpty) {
onUpdate(_attachments.first);
} else {
onUpdate(null);
}
},
child: imageAngleCard(icon, title, attachment),
).toShadowContainer(context, padding: 0),
),
if (attachment != null)
Positioned(
top: 4,
right: 4,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear),
iconSize: 18,
style: IconButton.styleFrom(elevation: 2, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: AppColor.red30, foregroundColor: Colors.white),
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
onPressed: () {
onUpdate(null);
},
)),
],
);
}
Widget imageAngleCard(String icon, String title, GenericAttachmentModel? attachment) {
if (attachment != null) {
Widget imageWidget = _getImageWidget(attachment.name!);
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: imageWidget.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: const DefaultAppBar(),
body: SafeArea(child: InteractiveViewer(child: imageWidget).center),
),
),
);
}));
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
icon.toSvgAsset(height: 50, width: 50),
Text(
title,
style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
),
],
);
}
Widget _getImageWidget(String imageUrl) {
// Check if it's base64 encoded (contains | separator)
if (imageUrl.contains('|')) {
try {
final base64Data = imageUrl.split('|').last;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
// Check if it's a local file path
if (CMRequestUtils.isLocalUrl(imageUrl)) {
return Image.file(
File(imageUrl),
fit: BoxFit.cover,
);
}
// Otherwise treat as network URL
return Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48);
},
);
}
} }

@ -12,7 +12,7 @@ class AssetItemGridView extends StatelessWidget {
final Asset device; final Asset device;
final Function(Asset) onPressed; final Function(Asset) onPressed;
const AssetItemGridView({Key? key,required this.device,required this.onPressed}) : super(key: key); const AssetItemGridView({Key? key, required this.device, required this.onPressed}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -43,6 +43,13 @@ class AssetItemGridView extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50), style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
), ),
if (context.userProvider.isEngineer && !(device.hasTrainingImageData ?? true)) ...[
2.height,
Text(
'*Upload Images for this asset',
style: AppTextStyles.tinyFont2.copyWith(color: AppColor.red30),
),
],
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -71,6 +78,6 @@ class AssetItemGridView extends StatelessWidget {
], ],
).expanded ).expanded
], ],
).onPress(() => onPressed(device)).toShadowContainer(context,padding: 12); ).onPress(() => onPressed(device)).toShadowContainer(context, padding: 12);
} }
} }

@ -41,7 +41,14 @@ class AssetItemListView extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
device.modelDefinition!.assetName!.cleanupWhitespace.capitalizeFirstOfEach.heading6(context), device.modelDefinition!.assetName!.cleanupWhitespace.capitalizeFirstOfEach.heading6(context),
8.height, if (context.userProvider.isEngineer && !(device.hasTrainingImageData ?? true)) ...[
2.height,
Text(
'*Upload Images for this asset',
style: AppTextStyles.tinyFont2.copyWith(color: AppColor.red30),
),
],
6.height,
"${context.translation.assetNumber} : ${device.assetNumber}".bodyText(context), "${context.translation.assetNumber} : ${device.assetNumber}".bodyText(context),
"${context.translation.modelName} : ${device.modelDefinition!.modelName}".cleanupWhitespace.capitalizeFirstOfEach.bodyText(context), "${context.translation.modelName} : ${device.modelDefinition!.modelName}".cleanupWhitespace.capitalizeFirstOfEach.bodyText(context),
], ],

@ -119,7 +119,7 @@ class _MyAssetsPageState extends State<MyAssetsPage> {
}).expanded, }).expanded,
16.width, 16.width,
Icon( Icon(
Icons.grid_view_outlined, showListView ? Icons.view_list_rounded : Icons.grid_view_rounded,
color: context.isDark ? AppColor.neutral10 : AppColor.neutral20, color: context.isDark ? AppColor.neutral10 : AppColor.neutral20,
).onPress(() => setState(() { ).onPress(() => setState(() {
showListView = !showListView; showListView = !showListView;

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_blur_detection/image_blur_detection.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
@ -324,6 +325,8 @@ class AttachmentPicker extends StatefulWidget {
final bool showAsListView; final bool showAsListView;
final bool pickMultiple; final bool pickMultiple;
Lookup? documentType; Lookup? documentType;
Widget? child;
bool checkQuality;
AttachmentPicker( AttachmentPicker(
{Key? key, {Key? key,
@ -339,6 +342,8 @@ class AttachmentPicker extends StatefulWidget {
this.showAsGrid = false, this.showAsGrid = false,
this.showAsListView = false, this.showAsListView = false,
this.pickMultiple = false, this.pickMultiple = false,
this.checkQuality = false,
this.child,
this.buttonColor}) this.buttonColor})
: super(key: key); : super(key: key);
@ -349,7 +354,13 @@ class AttachmentPicker extends StatefulWidget {
class _AttachmentPickerState extends State<AttachmentPicker> { class _AttachmentPickerState extends State<AttachmentPicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return widget.child != null
? widget.child!.onPress((widget.enabled == false)
? () {}
: widget.showAsGrid
? () => showFileSourceSheet()
: () => onFilePicker(widget.checkQuality))
: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12, spacing: 12,
children: [ children: [
@ -361,8 +372,8 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
onPressed: (widget.enabled == false) onPressed: (widget.enabled == false)
? () {} ? () {}
: widget.showAsGrid : widget.showAsGrid
? showFileSourceSheet ? () => showFileSourceSheet()
: onFilePicker), : () => onFilePicker(widget.checkQuality)),
if (widget.attachment.isNotEmpty) if (widget.attachment.isNotEmpty)
widget.showAsListView widget.showAsListView
? ListView.separated( ? ListView.separated(
@ -507,7 +518,7 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
); );
} }
onFilePicker() async { onFilePicker(bool checkQuality) async {
ImageSource? source = await showModalBottomSheet<ImageSource>( ImageSource? source = await showModalBottomSheet<ImageSource>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -658,6 +669,19 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
if (pickedFile != null) { if (pickedFile != null) {
File fileImage = File(pickedFile.path); File fileImage = File(pickedFile.path);
if (checkQuality) {
ImageQualityValidator imageQualityValidator = ImageQualityValidator();
final result = await imageQualityValidator.validate(await fileImage.readAsBytes());
if (!result.isValid) {
String issues = "Your image have following problems:";
result.issues.forEach((issue) => (issues += "\n- $issue"));
context.showInfoDialog(issues, title: "Bad Quality Found");
return;
}
}
widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType)); widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path, documentTypeId: widget.documentType));
if (widget.onChange != null) { if (widget.onChange != null) {
widget.onChange!(widget.attachment); widget.onChange!(widget.attachment);

@ -88,6 +88,7 @@ dependencies:
flutter_timezone: ^3.0.1 flutter_timezone: ^3.0.1
device_calendar: ^4.3.3 device_calendar: ^4.3.3
pie_chart: ^5.4.0 pie_chart: ^5.4.0
image_blur_detection: ^1.0.1
badges: ^3.1.1 badges: ^3.1.1
# buttons_tabbar: ^1.1.2 # buttons_tabbar: ^1.1.2
@ -136,6 +137,7 @@ flutter:
- assets/ - assets/
- assets/audio/ - assets/audio/
- assets/images/ - assets/images/
- assets/images/asset_image_upload/
- assets/lottie/ - assets/lottie/
- assets/subtitles/ - assets/subtitles/
- assets/rives/ - assets/rives/

Loading…
Cancel
Save