import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/constants/app_colors.dart'; import '../../core/constants/app_text_styles.dart'; import '../../data/services/api_service.dart'; import '../widgets/teacher_panel_scaffold.dart'; import 'dashboard_page.dart'; import 'license_page.dart'; import 'ticket_page.dart'; import 'transaction_page.dart'; class NewCoursePage extends StatefulWidget { const NewCoursePage({super.key}); @override State createState() => _NewCoursePageState(); } class _NewCoursePageState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); final TextEditingController _priceController = TextEditingController( text: '0', ); final TextEditingController _teacherPhoneController = TextEditingController(); final TextEditingController _altController = TextEditingController(); // Not in current API contract. Keep for future backend support. // final TextEditingController _supportLinkController = TextEditingController(); // final TextEditingController _accessLimitController = TextEditingController(text: '-1'); // final TextEditingController _noteController = TextEditingController(); _PickedLocalFile? _coverFile; _PickedLocalFile? _previewVideoFile; bool _isSubmitting = false; String? _errorMessage; @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); _priceController.dispose(); _teacherPhoneController.dispose(); _altController.dispose(); super.dispose(); } Future _pickCover() async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['jpg', 'jpeg', 'png', 'webp'], withData: true, ); if (result == null || result.files.isEmpty) { return; } final picked = result.files.first; if (picked.bytes == null) { setState(() { _errorMessage = 'خواندن فایل تصویر ناموفق بود.'; }); return; } setState(() { _coverFile = _PickedLocalFile( name: picked.name, bytes: picked.bytes!, sizeBytes: picked.size, ); _errorMessage = null; }); } Future _pickPreviewVideo() async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['mp4', 'mov', 'mkv', 'avi', 'webm'], withData: true, ); if (result == null || result.files.isEmpty) { return; } final picked = result.files.first; if (picked.bytes == null) { setState(() { _errorMessage = 'خواندن فایل ویدیو ناموفق بود.'; }); return; } const maxVideoBytes = 50 * 1024 * 1024; if (picked.size > maxVideoBytes) { setState(() { _errorMessage = 'حجم ویدیوی پیش‌نمایش باید حداکثر 50 مگابایت باشد.'; }); return; } setState(() { _previewVideoFile = _PickedLocalFile( name: picked.name, bytes: picked.bytes!, sizeBytes: picked.size, ); _errorMessage = null; }); } Future _submit() async { final title = _titleController.text.trim(); final description = _descriptionController.text.trim(); final priceRaw = _priceController.text.trim(); final teacherPhone = _teacherPhoneController.text.trim(); final alt = _altController.text.trim(); if (title.isEmpty) { setState(() => _errorMessage = 'عنوان دوره الزامی است.'); return; } if (title.length > 255) { setState(() => _errorMessage = 'حداکثر طول عنوان 255 کاراکتر است.'); return; } if (description.isEmpty) { setState(() => _errorMessage = 'توضیحات دوره الزامی است.'); return; } if (alt.isEmpty) { setState(() => _errorMessage = 'Alt الزامی است.'); return; } if (alt.length > 225) { setState(() => _errorMessage = 'حداکثر طول alt 225 کاراکتر است.'); return; } final price = num.tryParse(priceRaw); if (price == null || price < 0) { setState( () => _errorMessage = 'قیمت باید عدد معتبر بزرگ‌تر یا مساوی صفر باشد.', ); return; } if (_coverFile == null) { setState(() => _errorMessage = 'تصویر دوره الزامی است.'); return; } if (_previewVideoFile == null) { setState(() => _errorMessage = 'ویدیوی پیش‌نمایش الزامی است.'); return; } if (teacherPhone.isEmpty) { setState(() => _errorMessage = 'شماره موبایل مدرس الزامی است.'); return; } if (!RegExp(r'^09\d{9}$').hasMatch(teacherPhone)) { setState(() => _errorMessage = 'فرمت شماره مدرس صحیح نیست.'); return; } setState(() { _isSubmitting = true; _errorMessage = null; }); try { final apiService = Provider.of(context, listen: false); final response = await apiService.createCourse( title: title, description: description, price: price, coverBytes: _coverFile!.bytes, coverFileName: _coverFile!.name, previewVideoBytes: _previewVideoFile!.bytes, previewVideoFileName: _previewVideoFile!.name, teacherPhone: teacherPhone, alt: alt, ); if (!mounted) return; final serverMessage = (response['message'] ?? 'دوره با موفقیت ساخته شد.') .toString(); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(serverMessage))); Navigator.of(context).pop(); } catch (e) { setState(() { _errorMessage = e.toString(); }); } finally { if (mounted) { setState(() { _isSubmitting = false; }); } } } @override Widget build(BuildContext context) { final isDesktop = MediaQuery.of(context).size.width > 900; return TeacherPanelScaffold( selectedMenu: PanelMenu.course, onDashboardTap: () { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const DashboardPage()), ); }, onCourseTap: () { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } }, onLicenseTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const PanelLicensePage()), ); }, onTransactionTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const TransactionPage()), ); }, onTicketTap: () { Navigator.of( context, ).push(MaterialPageRoute(builder: (context) => const TicketPage())); }, child: SingleChildScrollView( padding: EdgeInsets.all(isDesktop ? 24 : 12), child: Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(10), ), padding: EdgeInsets.all(isDesktop ? 18 : 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back, color: AppColors.sidebarBg), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ const Icon( Icons.school, color: AppColors.sidebarBg, size: 18, ), const SizedBox(width: 6), Text( 'دوره جدید', style: AppTextStyles.headlineMedium.copyWith( color: AppColors.sidebarBg, fontSize: 20, ), ), ], ), const SizedBox(height: 14), Container(height: 1, color: Colors.black12), const SizedBox(height: 16), Text('اطلاعات دوره', style: AppTextStyles.headlineMedium), const SizedBox(height: 14), _buildLabel('نام دوره *'), const SizedBox(height: 6), _buildInput(_titleController), const SizedBox(height: 12), _buildLabel('توضیحات *'), const SizedBox(height: 6), _buildInput(_descriptionController, minLines: 3, maxLines: 5), const SizedBox(height: 12), _buildLabel('Alt *'), const SizedBox(height: 6), _buildInput(_altController), const SizedBox(height: 12), _buildLabel('قیمت *'), const SizedBox(height: 6), _buildInput( _priceController, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), ), const SizedBox(height: 12), _buildLabel('شماره موبایل مدرس *'), const SizedBox(height: 6), _buildInput( _teacherPhoneController, keyboardType: TextInputType.phone, ), const SizedBox(height: 12), _buildLabel('تصویر دوره (cover) *'), const SizedBox(height: 6), _buildFilePickerRow( fileName: _coverFile?.name ?? 'فایلی انتخاب نشده', sizeBytes: _coverFile?.sizeBytes, onPick: _pickCover, buttonLabel: 'انتخاب تصویر', ), const SizedBox(height: 12), _buildLabel('ویدیوی پیش‌نمایش (preview_video) *'), const SizedBox(height: 6), _buildFilePickerRow( fileName: _previewVideoFile?.name ?? 'فایلی انتخاب نشده', sizeBytes: _previewVideoFile?.sizeBytes, onPick: _pickPreviewVideo, buttonLabel: 'انتخاب ویدیو', ), const SizedBox(height: 6), Text( 'حداکثر حجم ویدیوی پیش‌نمایش: 50 مگابایت', style: AppTextStyles.bodyMedium, ), // Not in current API contract. Keep for future backend support. // const SizedBox(height: 22), // Text('تنظیمات پیش‌فرض لایسنس جدید', style: AppTextStyles.headlineMedium), // const SizedBox(height: 14), // _buildLabel('حد دسترسی'), // const SizedBox(height: 6), // _buildInput(_accessLimitController), // const SizedBox(height: 12), // _buildLabel('لینک پشتیبانی'), // const SizedBox(height: 6), // _buildInput(_supportLinkController), // const SizedBox(height: 22), // _buildLabel('یادداشت'), // const SizedBox(height: 8), // _buildInput(_noteController, minLines: 4, maxLines: 6), if (_errorMessage != null) ...[ const SizedBox(height: 16), Text( _errorMessage!, style: AppTextStyles.bodyMedium.copyWith(color: Colors.red), ), ], const SizedBox(height: 22), Wrap( spacing: 10, runSpacing: 10, children: [ SizedBox( width: isDesktop ? 140 : double.infinity, height: 44, child: ElevatedButton( onPressed: _isSubmitting ? null : _submit, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF43C61C), foregroundColor: Colors.white, ), child: _isSubmitting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('تایید'), ), ), SizedBox( width: isDesktop ? 140 : double.infinity, height: 44, child: ElevatedButton( onPressed: _isSubmitting ? null : () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFC95757), foregroundColor: Colors.white, ), child: const Text('انصراف'), ), ), ], ), ], ), ), ), ); } Widget _buildLabel(String text) { return Text(text, style: AppTextStyles.bodyLarge); } Widget _buildInput( TextEditingController controller, { TextInputType? keyboardType, int minLines = 1, int maxLines = 1, }) { return TextField( controller: controller, keyboardType: keyboardType, minLines: minLines, maxLines: maxLines, decoration: InputDecoration( isDense: true, filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.sidebarBg.withValues(alpha: 0.7), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.sidebarBg.withValues(alpha: 0.7), ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), ), ); } Widget _buildFilePickerRow({ required String fileName, required int? sizeBytes, required VoidCallback onPick, required String buttonLabel, }) { return Row( children: [ Expanded( child: Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all( color: AppColors.sidebarBg.withValues(alpha: 0.7), ), ), alignment: Alignment.centerRight, child: Text( sizeBytes == null ? fileName : '$fileName (${(sizeBytes / (1024 * 1024)).toStringAsFixed(2)} MB)', maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTextStyles.bodyLarge.copyWith( color: AppColors.sidebarBg, ), ), ), ), const SizedBox(width: 8), SizedBox( height: 44, child: OutlinedButton( onPressed: onPick, style: OutlinedButton.styleFrom( side: BorderSide( color: AppColors.sidebarBg.withValues(alpha: 0.7), ), ), child: Text(buttonLabel), ), ), ], ); } } class _PickedLocalFile { final String name; final Uint8List bytes; final int sizeBytes; _PickedLocalFile({ required this.name, required this.bytes, required this.sizeBytes, }); }