Merge branch 'refs/heads/main_production_ai_enhancement' into ui_ux_rollout_merge
# Conflicts: # lib/controllers/api_routes/urls.dart # lib/modules/cm_module/views/components/service_request_detail_view.dart # lib/views/widgets/dialogs/loading_dialog.dart # lib/views/widgets/equipment/asset_detail_page.dart # lib/views/widgets/equipment/asset_item_gridview.dart # lib/views/widgets/equipment/asset_item_listview.dart # lib/views/widgets/equipment/single_device_picker.dart # pubspec.yamlui_ux_rollout_merge
commit
8d45b02614
@ -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 |
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:test_sa/extensions/widget_extensions.dart';
|
||||||
|
|
||||||
|
class FullScreenImageViewer extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const FullScreenImageViewer(this.child, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
InteractiveViewer(clipBehavior: Clip.none, panEnabled: true, minScale: 0.5, maxScale: 4.0, child: child).center,
|
||||||
|
Positioned(top: MediaQuery.of(context).padding.top, left: 12.0, child: const Icon(Icons.clear).toShadowContainer(context, width: 40, padding: 8).onPress(() => Navigator.maybePop(context))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:test_sa/extensions/int_extensions.dart';
|
||||||
|
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||||
|
import 'package:test_sa/views/app_style/sizing.dart';
|
||||||
|
|
||||||
|
class LoadingWithTextDialog extends StatelessWidget {
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
const LoadingWithTextDialog({Key? key, this.message}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Material(
|
||||||
|
borderRadius: BorderRadius.circular(20.0),
|
||||||
|
child: Container(
|
||||||
|
height: 150.toScreenWidth,
|
||||||
|
width: 200.toScreenWidth,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.neutral30,
|
||||||
|
borderRadius: BorderRadius.circular(20.0),
|
||||||
|
boxShadow: [AppStyle.boxShadow],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(strokeWidth: 3),
|
||||||
|
16.height,
|
||||||
|
Text(
|
||||||
|
message ?? "Uploading...",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColor.textColor(context), height: 35 / 24, letterSpacing: -0.96),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,250 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:test_sa/controllers/providers/api/devices_provider.dart';
|
||||||
|
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';
|
||||||
|
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
|
||||||
|
import 'package:test_sa/views/full_screen_image_viewer.dart';
|
||||||
|
import 'package:test_sa/views/widgets/dialogs/dialog.dart';
|
||||||
|
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
|
||||||
|
|
||||||
|
class AssetImageUploadPage extends StatefulWidget {
|
||||||
|
int assetId;
|
||||||
|
TrainingImageData? attachments;
|
||||||
|
|
||||||
|
AssetImageUploadPage({Key? key, required this.assetId, this.attachments}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AssetImageUploadPageState createState() {
|
||||||
|
return _AssetImageUploadPageState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetImageUploadPageState extends State<AssetImageUploadPage> {
|
||||||
|
late TrainingImageData trainingImageData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
trainingImageData = widget.attachments ?? TrainingImageData(additionalImages: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const DefaultAppBar(title: "Upload Images"),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
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: "Additional Images",
|
||||||
|
attachment: trainingImageData.additionalImages!,
|
||||||
|
buttonColor: AppColor.black10,
|
||||||
|
onlyImages: true,
|
||||||
|
pickMultiple: true,
|
||||||
|
buttonIcon: 'image-plus'.toSvgAsset(color: context.isDark ? AppColor.primary10 : AppColor.neutral120),
|
||||||
|
onChange: (_attachments) {
|
||||||
|
trainingImageData.additionalImages = _attachments;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
).toShadowContainer(context),
|
||||||
|
],
|
||||||
|
).expanded,
|
||||||
|
FooterActionButton.footerContainer(
|
||||||
|
context: context,
|
||||||
|
child: AppFilledButton(
|
||||||
|
buttonColor: AppColor.primary10,
|
||||||
|
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: "I followed all the instructions and uploading the images in high quality, clear and not blurry.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
Utils.showLoadingWithTextDialog(context);
|
||||||
|
result = await Provider.of<AssetProvider>(context, listen: false).uploadAssetImages(widget.assetId, trainingImageData);
|
||||||
|
Utils.hideLoading(context);
|
||||||
|
if (result) {
|
||||||
|
Navigator.pop(context, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => context.fullScreenImageViewer(imageWidget)));
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue