288 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.dart';
 | |
| import 'package:gap/gap.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/models/post.dart';
 | |
| import 'package:island/models/wallet.dart';
 | |
| import 'package:island/pods/network.dart';
 | |
| import 'package:island/widgets/alert.dart';
 | |
| import 'package:island/widgets/content/cloud_files.dart';
 | |
| import 'package:island/widgets/content/sheet.dart';
 | |
| import 'package:island/widgets/payment/payment_overlay.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| 
 | |
| class PostAwardSheet extends HookConsumerWidget {
 | |
|   final SnPost post;
 | |
|   const PostAwardSheet({super.key, required this.post});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final messageController = useTextEditingController();
 | |
|     final amountController = useTextEditingController();
 | |
|     final selectedAttitude = useState<int>(0); // 0 for positive, 2 for negative
 | |
| 
 | |
|     return SheetScaffold(
 | |
|       titleText: 'awardPost'.tr(),
 | |
|       child: SingleChildScrollView(
 | |
|         padding: const EdgeInsets.all(16.0),
 | |
|         child: Column(
 | |
|           crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|           children: [
 | |
|             // Post Preview Section
 | |
|             _buildPostPreview(context),
 | |
|             const Gap(20),
 | |
| 
 | |
|             // Award Result Explanation
 | |
|             _buildAwardResultExplanation(context),
 | |
|             const Gap(20),
 | |
| 
 | |
|             Text(
 | |
|               'awardMessage'.tr(),
 | |
|               style: Theme.of(context).textTheme.titleMedium,
 | |
|             ),
 | |
|             const Gap(8),
 | |
|             TextField(
 | |
|               controller: messageController,
 | |
|               maxLines: 3,
 | |
|               decoration: InputDecoration(
 | |
|                 hintText: 'awardMessageHint'.tr(),
 | |
|                 border: const OutlineInputBorder(
 | |
|                   borderRadius: BorderRadius.all(Radius.circular(16)),
 | |
|                 ),
 | |
|               ),
 | |
|             ),
 | |
|             const Gap(16),
 | |
|             Text(
 | |
|               'awardAttitude'.tr(),
 | |
|               style: Theme.of(context).textTheme.titleMedium,
 | |
|             ),
 | |
|             const Gap(8),
 | |
|             SegmentedButton<int>(
 | |
|               segments: [
 | |
|                 ButtonSegment<int>(
 | |
|                   value: 0,
 | |
|                   label: Text('awardAttitudePositive'.tr()),
 | |
|                   icon: const Icon(Symbols.thumb_up),
 | |
|                 ),
 | |
|                 ButtonSegment<int>(
 | |
|                   value: 2,
 | |
|                   label: Text('awardAttitudeNegative'.tr()),
 | |
|                   icon: const Icon(Symbols.thumb_down),
 | |
|                 ),
 | |
|               ],
 | |
|               selected: {selectedAttitude.value},
 | |
|               onSelectionChanged: (Set<int> selection) {
 | |
|                 selectedAttitude.value = selection.first;
 | |
|               },
 | |
|             ),
 | |
|             const Gap(16),
 | |
|             Text(
 | |
|               'awardAmount'.tr(),
 | |
|               style: Theme.of(context).textTheme.titleMedium,
 | |
|             ),
 | |
|             const Gap(8),
 | |
|             TextField(
 | |
|               controller: amountController,
 | |
|               keyboardType: TextInputType.number,
 | |
|               decoration: InputDecoration(
 | |
|                 hintText: 'awardAmountHint'.tr(),
 | |
|                 border: const OutlineInputBorder(
 | |
|                   borderRadius: BorderRadius.all(Radius.circular(16)),
 | |
|                 ),
 | |
|                 suffixText: 'NSP',
 | |
|               ),
 | |
|             ),
 | |
|             const Gap(24),
 | |
|             SizedBox(
 | |
|               width: double.infinity,
 | |
|               child: FilledButton.icon(
 | |
|                 onPressed:
 | |
|                     () => _submitAward(
 | |
|                       context,
 | |
|                       ref,
 | |
|                       messageController,
 | |
|                       amountController,
 | |
|                       selectedAttitude.value,
 | |
|                     ),
 | |
|                 icon: const Icon(Symbols.star),
 | |
|                 label: Text('awardSubmit'.tr()),
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildPostPreview(BuildContext context) {
 | |
|     return Container(
 | |
|       padding: const EdgeInsets.all(12),
 | |
|       decoration: BoxDecoration(
 | |
|         color: Theme.of(context).colorScheme.surfaceContainerHighest,
 | |
|         borderRadius: BorderRadius.circular(12),
 | |
|         border: Border.all(
 | |
|           color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
 | |
|         ),
 | |
|       ),
 | |
|       child: Column(
 | |
|         crossAxisAlignment: CrossAxisAlignment.start,
 | |
|         children: [
 | |
|           Row(
 | |
|             children: [
 | |
|               Icon(
 | |
|                 Symbols.article,
 | |
|                 size: 20,
 | |
|                 color: Theme.of(context).colorScheme.primary,
 | |
|               ),
 | |
|               const Gap(8),
 | |
|               Text(
 | |
|                 'awardPostPreview'.tr(),
 | |
|                 style: Theme.of(
 | |
|                   context,
 | |
|                 ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|           const Gap(8),
 | |
|           Text(
 | |
|             post.content ?? 'awardNoContent'.tr(),
 | |
|             maxLines: 2,
 | |
|             overflow: TextOverflow.ellipsis,
 | |
|             style: Theme.of(context).textTheme.bodyMedium,
 | |
|           ),
 | |
|           ...[
 | |
|             const Gap(4),
 | |
|             Row(
 | |
|               spacing: 6,
 | |
|               children: [
 | |
|                 Text(
 | |
|                   'awardByPublisher'.tr(args: ['@${post.publisher.name}']),
 | |
|                   style: Theme.of(context).textTheme.bodySmall?.copyWith(
 | |
|                     color: Theme.of(context).colorScheme.onSurfaceVariant,
 | |
|                   ),
 | |
|                 ),
 | |
|                 ProfilePictureWidget(file: post.publisher.picture, radius: 8),
 | |
|               ],
 | |
|             ),
 | |
|           ],
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildAwardResultExplanation(BuildContext context) {
 | |
|     return Container(
 | |
|       padding: const EdgeInsets.all(12),
 | |
|       decoration: BoxDecoration(
 | |
|         color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
 | |
|         borderRadius: BorderRadius.circular(12),
 | |
|         border: Border.all(
 | |
|           color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
 | |
|         ),
 | |
|       ),
 | |
|       child: Column(
 | |
|         crossAxisAlignment: CrossAxisAlignment.start,
 | |
|         children: [
 | |
|           Row(
 | |
|             children: [
 | |
|               Icon(
 | |
|                 Symbols.info,
 | |
|                 size: 20,
 | |
|                 color: Theme.of(context).colorScheme.primary,
 | |
|               ),
 | |
|               const Gap(8),
 | |
|               Text(
 | |
|                 'awardBenefits'.tr(),
 | |
|                 style: Theme.of(context).textTheme.titleSmall?.copyWith(
 | |
|                   fontWeight: FontWeight.w600,
 | |
|                   color: Theme.of(context).colorScheme.primary,
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|           const Gap(8),
 | |
|           Text(
 | |
|             'awardBenefitsDescription'.tr(),
 | |
|             style: Theme.of(context).textTheme.bodySmall?.copyWith(
 | |
|               color: Theme.of(context).colorScheme.onSurfaceVariant,
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Future<void> _submitAward(
 | |
|     BuildContext context,
 | |
|     WidgetRef ref,
 | |
|     TextEditingController messageController,
 | |
|     TextEditingController amountController,
 | |
|     int selectedAttitude,
 | |
|   ) async {
 | |
|     // Get values from controllers
 | |
|     final message = messageController.text.trim();
 | |
|     final amountText = amountController.text.trim();
 | |
| 
 | |
|     // Validate inputs
 | |
|     if (amountText.isEmpty) {
 | |
|       showSnackBar('awardAmountRequired'.tr());
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     final amount = double.tryParse(amountText);
 | |
|     if (amount == null || amount <= 0) {
 | |
|       showSnackBar('awardAmountInvalid'.tr());
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (message.length > 4096) {
 | |
|       showSnackBar('awardMessageTooLong'.tr());
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       showLoadingModal(context);
 | |
| 
 | |
|       final client = ref.read(apiClientProvider);
 | |
| 
 | |
|       // Send award request
 | |
|       final awardResponse = await client.post(
 | |
|         '/sphere/posts/${post.id}/awards',
 | |
|         data: {
 | |
|           'amount': amount,
 | |
|           'attitude': selectedAttitude,
 | |
|           if (message.isNotEmpty) 'message': message,
 | |
|         },
 | |
|       );
 | |
| 
 | |
|       final orderId = awardResponse.data['order_id'] as String;
 | |
| 
 | |
|       // Fetch order details
 | |
|       final orderResponse = await client.get('/pass/orders/$orderId');
 | |
|       final order = SnWalletOrder.fromJson(orderResponse.data);
 | |
| 
 | |
|       if (context.mounted) {
 | |
|         hideLoadingModal(context);
 | |
| 
 | |
|         // Show payment overlay
 | |
|         final paidOrder = await PaymentOverlay.show(
 | |
|           context: context,
 | |
|           order: order,
 | |
|           enableBiometric: true,
 | |
|         );
 | |
| 
 | |
|         if (paidOrder != null && context.mounted) {
 | |
|           showSnackBar('awardSuccess'.tr());
 | |
|           Navigator.of(context).pop();
 | |
|         }
 | |
|       }
 | |
|     } catch (err) {
 | |
|       if (context.mounted) {
 | |
|         hideLoadingModal(context);
 | |
|         showErrorAlert(err);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 |