update theme

This commit is contained in:
Phuoc Nguyen
2025-12-02 15:20:54 +07:00
parent 12bd70479c
commit 49a41d24eb
78 changed files with 3263 additions and 2756 deletions

View File

@@ -14,6 +14,8 @@ import 'package:intl/intl.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/showrooms/presentation/providers/design_request_provider.dart';
import 'package:worker/features/showrooms/presentation/widgets/file_preview_item.dart';
import 'package:worker/features/showrooms/presentation/widgets/form_field_widget.dart';
/// Design Request Create Page
///
@@ -27,6 +29,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final formKey = useMemoized(() => GlobalKey<FormState>());
final projectNameController = useTextEditingController();
final areaController = useTextEditingController();
@@ -227,19 +230,19 @@ class DesignRequestCreatePage extends HookConsumerWidget {
}
return Scaffold(
backgroundColor: AppColors.grey50,
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
onPressed: () => Navigator.of(context).pop(),
),
centerTitle: false,
title: const Text(
title: Text(
'Tạo yêu cầu thiết kế mới',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
),
@@ -268,16 +271,16 @@ class DesignRequestCreatePage extends HookConsumerWidget {
children: [
Icon(
Icons.info_outline,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
const Text(
Text(
'Thông tin cơ bản',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -286,7 +289,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
const SizedBox(height: 20),
// Project Name
_FormField(
FormFieldWidget(
label: 'Tên dự án/Khách hàng',
required: true,
controller: projectNameController,
@@ -302,7 +305,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
const SizedBox(height: 20),
// Area
_FormField(
FormFieldWidget(
label: 'Diện tích (m²)',
required: true,
controller: areaController,
@@ -323,7 +326,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
const SizedBox(height: 20),
// Location
_FormField(
FormFieldWidget(
label: 'Khu vực (Tỉnh/ Thành phố)',
required: true,
controller: locationController,
@@ -343,14 +346,14 @@ class DesignRequestCreatePage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: const TextSpan(
text: TextSpan(
text: 'Phong cách mong muốn',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
children: [
children: const [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
@@ -367,22 +370,22 @@ class DesignRequestCreatePage extends HookConsumerWidget {
hintText: '-- Chọn phong cách --',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -444,12 +447,12 @@ class DesignRequestCreatePage extends HookConsumerWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'Ngân sách dự kiến',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -461,22 +464,22 @@ class DesignRequestCreatePage extends HookConsumerWidget {
hintText: '-- Chọn ngân sách --',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -525,14 +528,14 @@ class DesignRequestCreatePage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: const TextSpan(
text: TextSpan(
text: 'Thời hạn mong muốn',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
children: [
children: const [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
@@ -550,22 +553,22 @@ class DesignRequestCreatePage extends HookConsumerWidget {
suffixIcon: const Icon(Icons.calendar_today, size: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -606,16 +609,16 @@ class DesignRequestCreatePage extends HookConsumerWidget {
children: [
Icon(
Icons.edit_outlined,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
const Text(
Text(
'Yêu cầu chi tiết',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -628,14 +631,14 @@ class DesignRequestCreatePage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: const TextSpan(
text: TextSpan(
text: 'Ghi chú chi tiết',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
children: [
children: const [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
@@ -652,22 +655,22 @@ class DesignRequestCreatePage extends HookConsumerWidget {
'Mô tả chi tiết về yêu cầu thiết kế, số phòng, công năng sử dụng, sở thích cá nhân...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -706,16 +709,16 @@ class DesignRequestCreatePage extends HookConsumerWidget {
children: [
Icon(
Icons.cloud_upload_outlined,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
const Text(
Text(
'Đính kèm tài liệu',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -730,34 +733,34 @@ class DesignRequestCreatePage extends HookConsumerWidget {
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
width: 2,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(8),
color: AppColors.grey50,
color: colorScheme.surfaceContainerLowest,
),
child: Column(
children: [
Icon(
Icons.cloud_upload_outlined,
size: 32,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 12),
const Text(
Text(
'Nhấn để chọn file hoặc kéo thả vào đây',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
const Text(
Text(
'Hỗ trợ: JPG, PNG, PDF (Tối đa 10MB mỗi file)',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -771,7 +774,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
...selectedFiles.value.asMap().entries.map((entry) {
final index = entry.key;
final file = entry.value;
return _FilePreviewItem(
return FilePreviewItem(
file: file,
onRemove: () => removeFile(index),
);
@@ -790,10 +793,10 @@ class DesignRequestCreatePage extends HookConsumerWidget {
child: ElevatedButton(
onPressed: isSubmitting.value ? null : submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
disabledBackgroundColor: AppColors.primaryBlue.withValues(alpha: 0.7),
disabledForegroundColor: AppColors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.surface,
disabledBackgroundColor: colorScheme.primary.withValues(alpha: 0.7),
disabledForegroundColor: colorScheme.surface,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -804,12 +807,12 @@ class DesignRequestCreatePage extends HookConsumerWidget {
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.white,
color: colorScheme.surface,
),
),
const SizedBox(width: 12),
@@ -850,167 +853,3 @@ class DesignRequestCreatePage extends HookConsumerWidget {
}
}
/// Progress Step Widget
/// Form Field Widget
class _FormField extends StatelessWidget {
final String label;
final bool required;
final TextEditingController controller;
final String hint;
final TextInputType? keyboardType;
final String? Function(String?)? validator;
const _FormField({
required this.label,
this.required = false,
required this.controller,
required this.hint,
this.keyboardType,
this.validator,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
children: required
? const [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
),
]
: null,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
validator: validator,
),
],
);
}
}
/// File Preview Item Widget
class _FilePreviewItem extends StatelessWidget {
final PlatformFile file;
final VoidCallback onRemove;
const _FilePreviewItem({required this.file, required this.onRemove});
IconData _getFileIcon() {
final extension = file.extension?.toLowerCase();
if (extension == 'pdf') return Icons.picture_as_pdf;
if (extension == 'jpg' || extension == 'jpeg' || extension == 'png') {
return Icons.image;
}
return Icons.insert_drive_file;
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(6),
),
child: Icon(_getFileIcon(), color: AppColors.white, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatFileSize(file.size),
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
color: AppColors.danger,
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
),
],
),
);
}
}

View File

@@ -15,6 +15,9 @@ import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
import 'package:worker/features/showrooms/presentation/providers/design_request_provider.dart';
import 'package:worker/features/showrooms/presentation/widgets/description_item.dart';
import 'package:worker/features/showrooms/presentation/widgets/file_item.dart';
import 'package:worker/features/showrooms/presentation/widgets/image_viewer_dialog.dart';
/// Design Request Detail Page
///
@@ -161,7 +164,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
showDialog<void>(
context: context,
barrierColor: Colors.black87,
builder: (context) => _ImageViewerDialog(
builder: (context) => ImageViewerDialog(
images: images,
initialIndex: initialIndex,
),
@@ -171,21 +174,22 @@ class DesignRequestDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final detailAsync = ref.watch(designRequestDetailProvider(requestId));
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.grey50,
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
onPressed: () => Navigator.of(context).pop(),
),
centerTitle: false,
title: const Text(
title: Text(
'Chi tiết Yêu cầu',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
),
@@ -193,7 +197,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
actions: [
detailAsync.maybeWhen(
data: (request) => IconButton(
icon: const Icon(Icons.share, color: Colors.black),
icon: Icon(Icons.share, color: colorScheme.onSurface),
onPressed: () => _shareRequest(context, request),
),
orElse: () => const SizedBox.shrink(),
@@ -226,10 +230,10 @@ class DesignRequestDetailPage extends ConsumerWidget {
// Request ID
Text(
'#${request.id}',
style: const TextStyle(
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -238,9 +242,9 @@ class DesignRequestDetailPage extends ConsumerWidget {
if (request.dateline != null)
Text(
'Ngày gửi: ${request.dateline}',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
@@ -269,7 +273,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
),
const SizedBox(height: 24),
const Divider(height: 1, color: AppColors.grey100),
Divider(height: 1, color: colorScheme.surfaceContainerHighest),
const SizedBox(height: 24),
// Design Information Section
@@ -277,16 +281,16 @@ class DesignRequestDetailPage extends ConsumerWidget {
children: [
Icon(
Icons.info_outline,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
const Text(
Text(
'Thông tin thiết kế',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -295,14 +299,14 @@ class DesignRequestDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
// Description List
_DescriptionItem(
DescriptionItem(
label: 'Tên công trình:',
value: request.subject,
),
if (request.plainDescription.isNotEmpty) ...[
const SizedBox(height: 12),
_DescriptionItem(
DescriptionItem(
label: 'Mô tả chi tiết:',
value: request.plainDescription,
isMultiLine: true,
@@ -439,16 +443,16 @@ class DesignRequestDetailPage extends ConsumerWidget {
children: [
Icon(
Icons.attach_file,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
const Text(
Text(
'Tài liệu đính kèm',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -468,8 +472,8 @@ class DesignRequestDetailPage extends ConsumerWidget {
child: ElevatedButton.icon(
onPressed: () => _contactSupport(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -505,9 +509,9 @@ class DesignRequestDetailPage extends ConsumerWidget {
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
@@ -524,6 +528,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
}
Widget _buildFilesSection(BuildContext context, List<ProjectFile> files) {
final colorScheme = Theme.of(context).colorScheme;
// Separate images and other files
final images = files.where((f) => _isImageFile(f.fileUrl)).toList();
final otherFiles = files.where((f) => !_isImageFile(f.fileUrl)).toList();
@@ -553,13 +558,13 @@ class DesignRequestDetailPage extends ConsumerWidget {
imageUrl: image.fileUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => Container(
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
child: const Icon(Icons.error),
),
),
@@ -575,7 +580,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
// Other Files as file items
...otherFiles.map(
(file) => _FileItem(
(file) => FileItem(
fileUrl: file.fileUrl,
icon: _getFileIcon(file.fileUrl),
),
@@ -584,7 +589,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
// Show image files as file items too (for download/naming)
if (images.isNotEmpty && otherFiles.isEmpty)
...images.map(
(file) => _FileItem(
(file) => FileItem(
fileUrl: file.fileUrl,
icon: Icons.image,
),
@@ -593,246 +598,3 @@ class DesignRequestDetailPage extends ConsumerWidget {
);
}
}
/// Description Item Widget
class _DescriptionItem extends StatelessWidget {
const _DescriptionItem({
required this.label,
required this.value,
this.isMultiLine = false,
});
final String label;
final String value;
final bool isMultiLine;
@override
Widget build(BuildContext context) {
if (isMultiLine) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 15,
color: AppColors.grey900,
fontWeight: FontWeight.w500,
height: 1.6,
),
),
],
);
}
return Container(
padding: const EdgeInsets.only(bottom: 12),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.grey100)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 15,
color: AppColors.grey900,
fontWeight: FontWeight.w500,
height: 1.6,
),
),
),
],
),
);
}
}
/// File Item Widget
class _FileItem extends StatelessWidget {
const _FileItem({required this.fileUrl, required this.icon});
final String fileUrl;
final IconData icon;
String get fileName {
final uri = Uri.parse(fileUrl);
return uri.pathSegments.isNotEmpty ? uri.pathSegments.last : fileUrl;
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(6),
),
child: Icon(icon, color: Colors.white, size: 14),
),
const SizedBox(width: 12),
Expanded(
child: Text(
fileName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
/// Image Viewer Dialog with Swipe Navigation
class _ImageViewerDialog extends StatefulWidget {
const _ImageViewerDialog({
required this.images,
required this.initialIndex,
});
final List<ProjectFile> images;
final int initialIndex;
@override
State<_ImageViewerDialog> createState() => _ImageViewerDialogState();
}
class _ImageViewerDialogState extends State<_ImageViewerDialog> {
late PageController _pageController;
late int _currentIndex;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
color: Colors.black,
child: Stack(
children: [
// Main PageView
Center(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.images.length,
itemBuilder: (context, index) {
return Center(
child: CachedNetworkImage(
imageUrl: widget.images[index].fileUrl,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.white,
size: 48,
),
),
);
},
),
),
// Top bar with counter and close button
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_currentIndex + 1} / ${widget.images.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
),
),
),
],
),
),
);
}
}

View File

@@ -26,29 +26,30 @@ class ModelHouseDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final detailAsync = ref.watch(sampleProjectDetailProvider(modelId));
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(
icon: FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
color: colorScheme.onSurface,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text(
title: Text(
'Chi tiết Nhà mẫu',
style: TextStyle(color: Colors.black),
style: TextStyle(color: colorScheme.onSurface),
),
actions: [
detailAsync.maybeWhen(
data: (project) => IconButton(
icon: const FaIcon(
icon: FaIcon(
FontAwesomeIcons.shareNodes,
color: Colors.black,
color: colorScheme.onSurface,
size: 20,
),
onPressed: () => _shareModel(context, project),
@@ -58,7 +59,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
const SizedBox(width: AppSpacing.sm),
],
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
centerTitle: false,
),
body: detailAsync.when(
@@ -72,7 +73,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
// Project Information
_buildProjectInfo(project),
_buildProjectInfo(context, project),
const SizedBox(height: 16),
@@ -102,9 +103,9 @@ class ModelHouseDetailPage extends ConsumerWidget {
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
@@ -299,12 +300,13 @@ class ModelHouseDetailPage extends ConsumerWidget {
);
}
Widget _buildProjectInfo(SampleProject project) {
Widget _buildProjectInfo(BuildContext context, SampleProject project) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@@ -320,10 +322,10 @@ class ModelHouseDetailPage extends ConsumerWidget {
// Title
Text(
project.projectName,
style: const TextStyle(
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
if (project.plainDescription.isNotEmpty) ...[
@@ -331,9 +333,9 @@ class ModelHouseDetailPage extends ConsumerWidget {
// Description
Text(
project.plainDescription,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF4b5563),
color: colorScheme.onSurfaceVariant,
height: 1.6,
),
),
@@ -344,13 +346,14 @@ class ModelHouseDetailPage extends ConsumerWidget {
}
Widget _buildImageGallery(BuildContext context, SampleProject project) {
final colorScheme = Theme.of(context).colorScheme;
final images = project.filesList;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@@ -366,18 +369,18 @@ class ModelHouseDetailPage extends ConsumerWidget {
// Gallery Title
Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.images,
size: 18,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
const SizedBox(width: 8),
Text(
'Thư viện Hình ảnh (${images.length})',
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -405,13 +408,13 @@ class ModelHouseDetailPage extends ConsumerWidget {
imageUrl: image.fileUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => Container(
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
child: const Icon(Icons.error),
),
),

View File

@@ -92,41 +92,43 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.grey50,
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
onPressed: () => Navigator.of(context).pop(),
),
centerTitle: false,
title: const Text(
title: Text(
'Nhà mẫu',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
actions: [
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.black),
icon: Icon(Icons.info_outline, color: colorScheme.onSurface),
onPressed: _showInfoDialog,
),
const SizedBox(width: AppSpacing.sm),
],
bottom: TabBar(
controller: _tabController,
indicatorColor: AppColors.primaryBlue,
indicatorColor: colorScheme.primary,
indicatorWeight: 3,
labelColor: AppColors.primaryBlue,
labelColor: colorScheme.primary,
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
unselectedLabelColor: AppColors.grey500,
unselectedLabelColor: colorScheme.onSurfaceVariant,
unselectedLabelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -148,11 +150,11 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
return _tabController.index == 1
? FloatingActionButton(
onPressed: _createNewRequest,
backgroundColor: AppColors.primaryBlue,
backgroundColor: colorScheme.primary,
elevation: 4,
child: const Icon(
child: Icon(
Icons.add,
color: AppColors.white,
color: colorScheme.onPrimary,
size: 28,
),
)
@@ -169,28 +171,29 @@ class _LibraryTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final sampleProjectsAsync = ref.watch(sampleProjectsListProvider);
return sampleProjectsAsync.when(
data: (projects) {
if (projects.isEmpty) {
return const Center(
return Center(
child: Padding(
padding: EdgeInsets.all(40),
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.home_work_outlined,
size: 64,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
SizedBox(height: 16),
const SizedBox(height: 16),
Text(
'Chưa có mẫu nhà nào',
style: TextStyle(
fontSize: 16,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -229,9 +232,9 @@ class _LibraryTab extends ConsumerWidget {
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
@@ -255,6 +258,8 @@ class _LibraryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
@@ -282,26 +287,26 @@ class _LibraryCard extends StatelessWidget {
fit: BoxFit.cover,
placeholder: (context, url) => Container(
height: 200,
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
height: 200,
color: AppColors.grey100,
child: const Icon(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.image_not_supported,
size: 48,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
)
: Container(
height: 200,
color: AppColors.grey100,
child: const Icon(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.home_work,
size: 48,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
),
@@ -315,13 +320,13 @@ class _LibraryCard extends StatelessWidget {
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.primaryBlue.withValues(alpha: 0.9),
color: colorScheme.primary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
),
child: const Text(
child: Text(
'Xem 360°',
style: TextStyle(
color: AppColors.white,
color: colorScheme.onPrimary,
fontSize: 12,
fontWeight: FontWeight.w600,
),
@@ -340,10 +345,10 @@ class _LibraryCard extends StatelessWidget {
// Title
Text(
project.projectName,
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
@@ -352,9 +357,9 @@ class _LibraryCard extends StatelessWidget {
// Description
Text(
project.plainDescription,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
height: 1.5,
),
maxLines: 3,
@@ -377,28 +382,29 @@ class _DesignRequestsTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final requestsAsync = ref.watch(designRequestsListProvider);
return requestsAsync.when(
data: (requests) {
if (requests.isEmpty) {
return const Center(
return Center(
child: Padding(
padding: EdgeInsets.all(40),
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.design_services_outlined,
size: 64,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
SizedBox(height: 16),
const SizedBox(height: 16),
Text(
'Chưa có yêu cầu thiết kế nào',
style: TextStyle(
fontSize: 16,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -437,9 +443,9 @@ class _DesignRequestsTab extends ConsumerWidget {
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
@@ -489,6 +495,8 @@ class _RequestCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
@@ -510,10 +518,10 @@ class _RequestCard extends StatelessWidget {
Expanded(
child: Text(
'Mã yêu cầu: #${request.id}',
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
@@ -546,7 +554,7 @@ class _RequestCard extends StatelessWidget {
if (request.dateline != null)
Text(
'Deadline: ${request.dateline}',
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
),
const SizedBox(height: 8),
@@ -554,10 +562,10 @@ class _RequestCard extends StatelessWidget {
// Subject
Text(
request.subject,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
@@ -566,7 +574,7 @@ class _RequestCard extends StatelessWidget {
// Description
Text(
request.plainDescription,
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),