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