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 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://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/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/v5/mobile"; // v5 for data segregation
// 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;
try {
response =
await ApiManager.instance.multiPart(URLs.saveAssetImages + "?AssetId=$assetId", body: {}, files: attachments.map((attachment) => MultipartFile.fromPath("Files", attachment.name!)).toList());
List<Future<MultipartFile>> files = [];
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) {
// client's request was successfully received
// Map<String, dynamic> assetData = json.decode(response.body)["data"];

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

@ -1,4 +1,5 @@
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';
class AssetByIdModel {
@ -63,6 +64,8 @@ class AssetByIdModel {
String? modelPhoto;
String? nextPMDate;
String? lastPMDate;
bool? hasTrainingImageData;
TrainingImageData? trainingImageData;
AssetByIdModel(
{this.multiAssets,
@ -125,9 +128,13 @@ class AssetByIdModel {
this.assetPhoto,
this.modelPhoto,
this.nextPMDate,
this.lastPMDate});
this.lastPMDate,
this.trainingImageData,
this.hasTrainingImageData});
AssetByIdModel.fromJson(Map<String, dynamic> json) {
print("assetModel!.fromJson:${json['hasTrainingImageData']}");
if (json['multiAssets'] != null) {
multiAssets = <MultiAssets>[];
json['multiAssets'].forEach((v) {
@ -204,6 +211,8 @@ class AssetByIdModel {
modelPhoto = json['modelPhoto'];
nextPMDate = json['nextPMDate'];
lastPMDate = json['lastPMDate'];
hasTrainingImageData = json['hasTrainingImageData'];
trainingImageData = json['trainingImageData'] != null ? TrainingImageData.fromJson(json['trainingImageData']) : null;
}
Map<String, dynamic> toJson() {
@ -301,6 +310,8 @@ class AssetByIdModel {
data['modelPhoto'] = modelPhoto;
data['nextPMDate'] = nextPMDate;
data['lastPMDate'] = lastPMDate;
data['hasTrainingImageData'] = hasTrainingImageData;
data['trainingImageData'] = trainingImageData?.toJson();
return data;
}
}
@ -789,3 +800,37 @@ class DemoRequest {
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 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/lookup.dart';
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;
String? name;
@ -11,11 +12,14 @@ class GenericAttachmentModel {
String? originalName;
Lookup? documentTypeId;
GenericAttachmentModel.fromJson(Map<String, dynamic> json) {
GenericAttachmentModel.fromJson(Map<String, dynamic> json, {bool convertToLink = false}) {
id = json['id'];
name = json['name'];
name = json['name'] ?? json['imageName'];
createdBy = json['createdBy'];
originalName = json['originalName'];
originalName = json['originalName'] ?? json['imageOriginalName'];
if (convertToLink) {
name = URLs.getFileUrl(name);
}
}
Map<String, dynamic> toJson() {

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

@ -206,6 +206,13 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
],
),
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(
'${context.translation.assetName}: ${workOrder.assetNdModel!.name?.cleanupWhitespace?.capitalizeFirstOfEach}',
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),

@ -171,68 +171,67 @@ class _SearchDevicePageState extends State<SearchDevicePage> {
).expanded,
if (widget.multiSelection && selectedAssets.isNotEmpty)
FooterActionButton.footerContainer(
context: context,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: AppFilledButton(
label: "Clear",
buttonColor: AppColor.white60,
textColor: AppColor.black10,
loading: false,
onPressed: () async {
selectedAssets.clear();
setState(() {});
},
context: context,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: AppFilledButton(
label: "Clear",
buttonColor: AppColor.white60,
textColor: AppColor.black10,
loading: false,
onPressed: () async {
selectedAssets.clear();
setState(() {});
},
),
),
),
12.width,
Expanded(
child: AppFilledButton(
label: context.translation.select,
buttonColor: AppColor.primary10,
loading: false,
onPressed: () async {
Navigator.of(context).pop();
List<Asset> assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList();
Navigator.of(context).pop(assets);
},
12.width,
Expanded(
child: AppFilledButton(
label: context.translation.select,
buttonColor: AppColor.primary10,
loading: false,
onPressed: () async {
Navigator.of(context).pop();
List<Asset> assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList();
Navigator.of(context).pop(assets);
},
),
),
),
],
)
),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: AppFilledButton(
// label: "Clear",
// buttonColor: AppColor.white60,
// textColor: AppColor.black10,
// loading: false,
// onPressed: () async {
// selectedAssets.clear();
// setState(() {});
// },
// ),
// ),
// 12.width,
// Expanded(
// child: AppFilledButton(
// label: context.translation.select,
// buttonColor: AppColor.primary10,
// loading: false,
// onPressed: () async {
// Navigator.of(context).pop();
// List<Asset> assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList();
// Navigator.of(context).pop(assets);
// },
// ),
// ),
// ],
// ).toShadowContainer(context),
],
)),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: AppFilledButton(
// label: "Clear",
// buttonColor: AppColor.white60,
// textColor: AppColor.black10,
// loading: false,
// onPressed: () async {
// selectedAssets.clear();
// setState(() {});
// },
// ),
// ),
// 12.width,
// Expanded(
// child: AppFilledButton(
// label: context.translation.select,
// buttonColor: AppColor.primary10,
// loading: false,
// onPressed: () async {
// Navigator.of(context).pop();
// List<Asset> assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList();
// Navigator.of(context).pop(assets);
// },
// ),
// ),
// ],
// ).toShadowContainer(context),
],
),
),

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

@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/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/widget_extensions.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/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/new_views/app_style/app_color.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 {
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
_AssetImageUploadPageState createState() {
@ -26,12 +31,12 @@ class AssetImageUploadPage extends StatefulWidget {
}
class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
List<GenericAttachmentModel> attachments = [];
late TrainingImageData trainingImageData;
@override
void initState() {
super.initState();
// attachments = widget.attachments;
trainingImageData = widget.attachments ?? TrainingImageData(additionalImages: []);
}
@override
@ -48,16 +53,68 @@ class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
ListView(
padding: const EdgeInsets.all(16),
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(
label: "Import",
attachment: attachments,
label: "Additional Images",
attachment: trainingImageData.additionalImages!,
buttonColor: AppColor.black10,
onlyImages: true,
pickMultiple: true,
showAsListView: true,
buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120),
onChange: (_attachments) {
attachments = _attachments;
trainingImageData.additionalImages = _attachments;
setState(() {});
},
).toShadowContainer(context),
@ -67,18 +124,19 @@ class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
disableButton: attachments.length < 4,
label: "Upload Images",
disableButton: trainingImageData.leftImage == null || trainingImageData.rightImage == null || trainingImageData.upImage == null || trainingImageData.downImage == null,
label: "Upload",
onPressed: () async {
bool result = await showDialog(
context: context,
builder: (_) => AAlertDialog(
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?"),
title: context.translation.confirm,
content: "I followed all the instructions and uploading the images in high quality, clear and not blurry.",
),
);
if (result) {
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);
if (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 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
Widget build(BuildContext context) {
@ -43,6 +43,13 @@ class AssetItemGridView extends StatelessWidget {
overflow: TextOverflow.ellipsis,
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(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -71,6 +78,6 @@ class AssetItemGridView extends StatelessWidget {
],
).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,
children: [
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.modelName} : ${device.modelDefinition!.modelName}".cleanupWhitespace.capitalizeFirstOfEach.bodyText(context),
],

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

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_blur_detection/image_blur_detection.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
@ -324,6 +325,8 @@ class AttachmentPicker extends StatefulWidget {
final bool showAsListView;
final bool pickMultiple;
Lookup? documentType;
Widget? child;
bool checkQuality;
AttachmentPicker(
{Key? key,
@ -339,6 +342,8 @@ class AttachmentPicker extends StatefulWidget {
this.showAsGrid = false,
this.showAsListView = false,
this.pickMultiple = false,
this.checkQuality = false,
this.child,
this.buttonColor})
: super(key: key);
@ -349,72 +354,78 @@ class AttachmentPicker extends StatefulWidget {
class _AttachmentPickerState extends State<AttachmentPicker> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
AppDashedButton(
title: widget.label,
height: widget.buttonHeight,
buttonColor: widget.buttonColor,
icon: widget.buttonIcon,
onPressed: (widget.enabled == false)
? () {}
: widget.showAsGrid
? showFileSourceSheet
: onFilePicker),
if (widget.attachment.isNotEmpty)
widget.showAsListView
? ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 0),
itemBuilder: (cxt, index) {
File image = File(widget.attachment[index].name!);
return MultiFilesPickerItem(
file: image,
showAsListView: widget.showAsListView,
enabled: widget.enabled,
documentType: widget.attachment[index].documentTypeId?.name,
onRemoveTap: (image) {
if (!widget.enabled) {
return;
}
widget.attachment.removeAt(index);
if (widget.onChange != null) {
widget.onChange!(widget.attachment);
}
setState(() {});
},
);
},
separatorBuilder: (cxt, index) => 8.height,
itemCount: widget.attachment.length)
: Wrap(
spacing: 8.toScreenWidth,
children: List.generate(
widget.attachment.length,
(index) {
File image = File(widget.attachment[index].name!);
return MultiFilesPickerItem(
file: image,
enabled: widget.enabled,
onRemoveTap: (image) {
if (!widget.enabled) {
return;
}
widget.attachment.removeAt(index);
if (widget.onChange != null) {
widget.onChange!(widget.attachment);
}
setState(() {});
return widget.child != null
? widget.child!.onPress((widget.enabled == false)
? () {}
: widget.showAsGrid
? () => showFileSourceSheet()
: () => onFilePicker(widget.checkQuality))
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
AppDashedButton(
title: widget.label,
height: widget.buttonHeight,
buttonColor: widget.buttonColor,
icon: widget.buttonIcon,
onPressed: (widget.enabled == false)
? () {}
: widget.showAsGrid
? () => showFileSourceSheet()
: () => onFilePicker(widget.checkQuality)),
if (widget.attachment.isNotEmpty)
widget.showAsListView
? ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 0),
itemBuilder: (cxt, index) {
File image = File(widget.attachment[index].name!);
return MultiFilesPickerItem(
file: image,
showAsListView: widget.showAsListView,
enabled: widget.enabled,
documentType: widget.attachment[index].documentTypeId?.name,
onRemoveTap: (image) {
if (!widget.enabled) {
return;
}
widget.attachment.removeAt(index);
if (widget.onChange != null) {
widget.onChange!(widget.attachment);
}
setState(() {});
},
);
},
);
},
),
).paddingOnly(top: 0),
],
);
separatorBuilder: (cxt, index) => 8.height,
itemCount: widget.attachment.length)
: Wrap(
spacing: 8.toScreenWidth,
children: List.generate(
widget.attachment.length,
(index) {
File image = File(widget.attachment[index].name!);
return MultiFilesPickerItem(
file: image,
enabled: widget.enabled,
onRemoveTap: (image) {
if (!widget.enabled) {
return;
}
widget.attachment.removeAt(index);
if (widget.onChange != null) {
widget.onChange!(widget.attachment);
}
setState(() {});
},
);
},
),
).paddingOnly(top: 0),
],
);
}
fromFilePicker() async {
@ -507,7 +518,7 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
);
}
onFilePicker() async {
onFilePicker(bool checkQuality) async {
ImageSource? source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (BuildContext context) {
@ -658,6 +669,19 @@ class _AttachmentPickerState extends State<AttachmentPicker> {
if (pickedFile != null) {
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));
if (widget.onChange != null) {
widget.onChange!(widget.attachment);

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

Loading…
Cancel
Save