362 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:dropdown_button2/dropdown_button2.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.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/widgets/content/sheet.dart';
 | |
| import 'package:island/widgets/post/compose_shared.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| 
 | |
| class ComposeEmbedSheet extends HookConsumerWidget {
 | |
|   final ComposeState state;
 | |
| 
 | |
|   const ComposeEmbedSheet({super.key, required this.state});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final theme = Theme.of(context);
 | |
|     final colorScheme = theme.colorScheme;
 | |
| 
 | |
|     // Listen to embed view changes
 | |
|     final currentEmbedView = useValueListenable(state.embedView);
 | |
| 
 | |
|     // Form state
 | |
|     final uriController = useTextEditingController();
 | |
|     final aspectRatioController = useTextEditingController();
 | |
|     final selectedRenderer = useState<PostEmbedViewRenderer>(
 | |
|       PostEmbedViewRenderer.webView,
 | |
|     );
 | |
|     final tabController = useTabController(initialLength: 2);
 | |
|     final iframeController = useTextEditingController();
 | |
| 
 | |
|     void clearForm() {
 | |
|       uriController.clear();
 | |
|       aspectRatioController.clear();
 | |
|       iframeController.clear();
 | |
|       selectedRenderer.value = PostEmbedViewRenderer.webView;
 | |
|     }
 | |
| 
 | |
|     // Populate form when embed view changes
 | |
|     useEffect(() {
 | |
|       if (currentEmbedView != null) {
 | |
|         uriController.text = currentEmbedView.uri;
 | |
|         aspectRatioController.text =
 | |
|             currentEmbedView.aspectRatio?.toString() ?? '';
 | |
|         selectedRenderer.value = currentEmbedView.renderer;
 | |
|       } else {
 | |
|         clearForm();
 | |
|       }
 | |
|       return null;
 | |
|     }, [currentEmbedView]);
 | |
| 
 | |
|     void saveEmbedView() {
 | |
|       final uri = uriController.text.trim();
 | |
|       if (uri.isEmpty) {
 | |
|         ScaffoldMessenger.of(
 | |
|           context,
 | |
|         ).showSnackBar(SnackBar(content: Text('embedUriRequired'.tr())));
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       final aspectRatio =
 | |
|           aspectRatioController.text.trim().isNotEmpty
 | |
|               ? double.tryParse(aspectRatioController.text.trim())
 | |
|               : null;
 | |
| 
 | |
|       final embedView = SnPostEmbedView(
 | |
|         uri: uri,
 | |
|         aspectRatio: aspectRatio,
 | |
|         renderer: selectedRenderer.value,
 | |
|       );
 | |
| 
 | |
|       if (currentEmbedView != null) {
 | |
|         ComposeLogic.updateEmbedView(state, embedView);
 | |
|       } else {
 | |
|         ComposeLogic.setEmbedView(state, embedView);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     void parseIframe() {
 | |
|       final iframe = iframeController.text.trim();
 | |
|       if (iframe.isEmpty) return;
 | |
| 
 | |
|       final srcMatch = RegExp(r'src="([^"]*)"').firstMatch(iframe);
 | |
|       final widthMatch = RegExp(r'width="([^"]*)"').firstMatch(iframe);
 | |
|       final heightMatch = RegExp(r'height="([^"]*)"').firstMatch(iframe);
 | |
| 
 | |
|       if (srcMatch != null) {
 | |
|         uriController.text = srcMatch.group(1)!;
 | |
|       }
 | |
| 
 | |
|       if (widthMatch != null && heightMatch != null) {
 | |
|         final w = double.tryParse(widthMatch.group(1)!);
 | |
|         final h = double.tryParse(heightMatch.group(1)!);
 | |
|         if (w != null && h != null && h != 0) {
 | |
|           aspectRatioController.text = (w / h).toStringAsFixed(3);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       tabController.animateTo(1);
 | |
|     }
 | |
| 
 | |
|     void deleteEmbed(BuildContext context) {
 | |
|       showDialog(
 | |
|         context: context,
 | |
|         builder:
 | |
|             (dialogContext) => AlertDialog(
 | |
|               title: Text('deleteEmbed').tr(),
 | |
|               content: Text('deleteEmbedConfirm').tr(),
 | |
|               actions: [
 | |
|                 TextButton(
 | |
|                   onPressed: () => Navigator.of(dialogContext).pop(),
 | |
|                   child: Text('cancel').tr(),
 | |
|                 ),
 | |
|                 TextButton(
 | |
|                   onPressed: () {
 | |
|                     ComposeLogic.deleteEmbedView(state);
 | |
|                     clearForm();
 | |
|                     Navigator.of(dialogContext).pop();
 | |
|                   },
 | |
|                   style: TextButton.styleFrom(
 | |
|                     foregroundColor: Theme.of(context).colorScheme.error,
 | |
|                   ),
 | |
|                   child: Text('delete').tr(),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return SheetScaffold(
 | |
|       titleText: 'embedView'.tr(),
 | |
|       heightFactor: 0.7,
 | |
|       child: Column(
 | |
|         children: [
 | |
|           // Header with save button when editing
 | |
|           if (currentEmbedView != null)
 | |
|             Container(
 | |
|               padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
 | |
|               color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | |
|               child: Row(
 | |
|                 children: [
 | |
|                   Expanded(
 | |
|                     child: Text(
 | |
|                       'editEmbed'.tr(),
 | |
|                       style: theme.textTheme.titleMedium,
 | |
|                     ),
 | |
|                   ),
 | |
|                   TextButton(
 | |
|                     onPressed: saveEmbedView,
 | |
|                     style: ButtonStyle(visualDensity: VisualDensity.compact),
 | |
|                     child: Text('save'.tr()),
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
| 
 | |
|           // Tab bar
 | |
|           TabBar(
 | |
|             controller: tabController,
 | |
|             tabs: [Tab(text: 'auto'.tr()), Tab(text: 'manual'.tr())],
 | |
|           ),
 | |
| 
 | |
|           // Content area
 | |
|           Expanded(
 | |
|             child: TabBarView(
 | |
|               controller: tabController,
 | |
|               children: [
 | |
|                 // Auto tab
 | |
|                 SingleChildScrollView(
 | |
|                   padding: const EdgeInsets.all(16),
 | |
|                   child: Column(
 | |
|                     crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                     children: [
 | |
|                       TextField(
 | |
|                         controller: iframeController,
 | |
|                         decoration: InputDecoration(
 | |
|                           labelText: 'iframeCode'.tr(),
 | |
|                           hintText: 'iframeCodeHint'.tr(),
 | |
|                           border: OutlineInputBorder(
 | |
|                             borderRadius: BorderRadius.circular(8),
 | |
|                           ),
 | |
|                         ),
 | |
|                         maxLines: 5,
 | |
|                       ),
 | |
|                       const Gap(16),
 | |
|                       SizedBox(
 | |
|                         width: double.infinity,
 | |
|                         child: FilledButton.icon(
 | |
|                           onPressed: parseIframe,
 | |
|                           icon: const Icon(Symbols.auto_fix),
 | |
|                           label: Text('parseIframe'.tr()),
 | |
|                         ),
 | |
|                       ),
 | |
|                     ],
 | |
|                   ),
 | |
|                 ),
 | |
|                 // Manual tab
 | |
|                 SingleChildScrollView(
 | |
|                   padding: const EdgeInsets.all(16),
 | |
|                   child: Column(
 | |
|                     crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                     children: [
 | |
|                       // Form fields
 | |
|                       TextField(
 | |
|                         controller: uriController,
 | |
|                         decoration: InputDecoration(
 | |
|                           labelText: 'embedUri'.tr(),
 | |
|                           hintText: 'https://example.com',
 | |
|                           border: OutlineInputBorder(
 | |
|                             borderRadius: BorderRadius.circular(8),
 | |
|                           ),
 | |
|                         ),
 | |
|                         keyboardType: TextInputType.url,
 | |
|                       ),
 | |
|                       const Gap(16),
 | |
|                       TextField(
 | |
|                         controller: aspectRatioController,
 | |
|                         decoration: InputDecoration(
 | |
|                           labelText: 'aspectRatio'.tr(),
 | |
|                           hintText: '16/9 = 1.777',
 | |
|                           border: OutlineInputBorder(
 | |
|                             borderRadius: BorderRadius.circular(8),
 | |
|                           ),
 | |
|                         ),
 | |
|                         keyboardType: TextInputType.numberWithOptions(
 | |
|                           decimal: true,
 | |
|                         ),
 | |
|                         inputFormatters: [
 | |
|                           FilteringTextInputFormatter.allow(
 | |
|                             RegExp(r'^\d*\.?\d*$'),
 | |
|                           ),
 | |
|                         ],
 | |
|                       ),
 | |
|                       const Gap(16),
 | |
|                       DropdownButtonFormField2<PostEmbedViewRenderer>(
 | |
|                         value: selectedRenderer.value,
 | |
|                         decoration: InputDecoration(
 | |
|                           labelText: 'renderer'.tr(),
 | |
|                           border: OutlineInputBorder(
 | |
|                             borderRadius: BorderRadius.circular(8),
 | |
|                           ),
 | |
|                         ),
 | |
|                         selectedItemBuilder: (context) {
 | |
|                           return PostEmbedViewRenderer.values.map((renderer) {
 | |
|                             return Text(renderer.name).tr();
 | |
|                           }).toList();
 | |
|                         },
 | |
|                         menuItemStyleData: MenuItemStyleData(
 | |
|                           padding: EdgeInsets.zero,
 | |
|                         ),
 | |
|                         items:
 | |
|                             PostEmbedViewRenderer.values.map((renderer) {
 | |
|                               return DropdownMenuItem(
 | |
|                                 value: renderer,
 | |
|                                 child: Text(
 | |
|                                   renderer.name,
 | |
|                                 ).tr().padding(horizontal: 20),
 | |
|                               );
 | |
|                             }).toList(),
 | |
|                         onChanged: (value) {
 | |
|                           if (value != null) {
 | |
|                             selectedRenderer.value = value;
 | |
|                           }
 | |
|                         },
 | |
|                       ),
 | |
| 
 | |
|                       // Current embed view display (when exists)
 | |
|                       if (currentEmbedView != null) ...[
 | |
|                         const Gap(32),
 | |
|                         Text(
 | |
|                           'currentEmbed'.tr(),
 | |
|                           style: theme.textTheme.titleMedium,
 | |
|                         ).padding(horizontal: 4),
 | |
|                         const Gap(8),
 | |
|                         Card(
 | |
|                           margin: EdgeInsets.zero,
 | |
|                           color:
 | |
|                               Theme.of(
 | |
|                                 context,
 | |
|                               ).colorScheme.surfaceContainerHigh,
 | |
|                           child: Padding(
 | |
|                             padding: const EdgeInsets.only(
 | |
|                               left: 16,
 | |
|                               right: 16,
 | |
|                               bottom: 12,
 | |
|                               top: 4,
 | |
|                             ),
 | |
|                             child: Column(
 | |
|                               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                               children: [
 | |
|                                 Row(
 | |
|                                   children: [
 | |
|                                     Icon(
 | |
|                                       currentEmbedView.renderer ==
 | |
|                                               PostEmbedViewRenderer.webView
 | |
|                                           ? Symbols.web
 | |
|                                           : Symbols.web,
 | |
|                                       color: colorScheme.primary,
 | |
|                                     ),
 | |
|                                     const Gap(12),
 | |
|                                     Expanded(
 | |
|                                       child: Text(
 | |
|                                         currentEmbedView.uri,
 | |
|                                         style: theme.textTheme.bodyMedium,
 | |
|                                         maxLines: 1,
 | |
|                                         overflow: TextOverflow.ellipsis,
 | |
|                                       ),
 | |
|                                     ),
 | |
|                                     IconButton(
 | |
|                                       icon: const Icon(Symbols.delete),
 | |
|                                       onPressed: () => deleteEmbed(context),
 | |
|                                       tooltip: 'delete'.tr(),
 | |
|                                       color: colorScheme.error,
 | |
|                                     ),
 | |
|                                   ],
 | |
|                                 ),
 | |
|                                 const Gap(12),
 | |
|                                 Text(
 | |
|                                   'aspectRatio'.tr(),
 | |
|                                   style: theme.textTheme.labelMedium?.copyWith(
 | |
|                                     color: colorScheme.onSurfaceVariant,
 | |
|                                   ),
 | |
|                                 ),
 | |
|                                 const Gap(4),
 | |
|                                 Text(
 | |
|                                   currentEmbedView.aspectRatio != null
 | |
|                                       ? currentEmbedView.aspectRatio!
 | |
|                                           .toStringAsFixed(2)
 | |
|                                       : 'notSet'.tr(),
 | |
|                                   style: theme.textTheme.bodyMedium,
 | |
|                                 ),
 | |
|                               ],
 | |
|                             ),
 | |
|                           ),
 | |
|                         ),
 | |
|                       ] else ...[
 | |
|                         const Gap(16),
 | |
|                         SizedBox(
 | |
|                           width: double.infinity,
 | |
|                           child: FilledButton.icon(
 | |
|                             onPressed: saveEmbedView,
 | |
|                             icon: const Icon(Symbols.add),
 | |
|                             label: Text('addEmbed'.tr()),
 | |
|                           ),
 | |
|                         ),
 | |
|                       ],
 | |
|                     ],
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |