♻️ Refactored the post compose sheet
This commit is contained in:
		| @@ -8,7 +8,7 @@ import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/services/event_bus.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/post/compose_dialog.dart'; | ||||
| import 'package:island/widgets/post/compose_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| enum FabMenuType { main, chat, realm } | ||||
| @@ -166,7 +166,7 @@ class FabMenu extends HookConsumerWidget { | ||||
|               title: Text('postCompose').tr(), | ||||
|               onTap: () async { | ||||
|                 Navigator.of(context).pop(); | ||||
|                 await PostComposeDialog.show(context); | ||||
|                 await PostComposeSheet.show(context); | ||||
|               }, | ||||
|             ), | ||||
|             const Divider(), | ||||
|   | ||||
| @@ -36,7 +36,8 @@ class PostComposeCard extends HookConsumerWidget { | ||||
|   final VoidCallback? onCancel; | ||||
|   final Function()? onSubmit; | ||||
|   final Function(ComposeState)? onStateChanged; | ||||
|   final bool isDialog; | ||||
|   final bool isContained; | ||||
|   final bool showHeader; | ||||
|  | ||||
|   const PostComposeCard({ | ||||
|     super.key, | ||||
| @@ -45,7 +46,8 @@ class PostComposeCard extends HookConsumerWidget { | ||||
|     this.onCancel, | ||||
|     this.onSubmit, | ||||
|     this.onStateChanged, | ||||
|     this.isDialog = false, | ||||
|     this.isContained = false, | ||||
|     this.showHeader = true, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -169,14 +171,12 @@ class PostComposeCard extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final maxHeight = math.min( | ||||
|       640.0, | ||||
|       MediaQuery.of(context).size.height * (isDialog ? 0.8 : 0.72), | ||||
|     ); | ||||
|     final maxHeight = math.min(640.0, MediaQuery.of(context).size.height * 0.8); | ||||
|  | ||||
|     return Card( | ||||
|       margin: EdgeInsets.zero, | ||||
|       color: isDialog ? Theme.of(context).colorScheme.surfaceContainer : null, | ||||
|       color: isContained ? Colors.transparent : null, | ||||
|       elevation: isContained ? 0 : null, | ||||
|       child: Container( | ||||
|         constraints: BoxConstraints(maxHeight: maxHeight), | ||||
|         child: Column( | ||||
| @@ -184,75 +184,81 @@ class PostComposeCard extends HookConsumerWidget { | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             // Header with actions | ||||
|             Container( | ||||
|               height: 65, | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||
|               decoration: BoxDecoration( | ||||
|                 border: Border( | ||||
|                   bottom: BorderSide( | ||||
|                     color: theme.colorScheme.outline.withOpacity(0.2), | ||||
|             if (showHeader) | ||||
|               Container( | ||||
|                 height: 65, | ||||
|                 padding: const EdgeInsets.symmetric( | ||||
|                   horizontal: 16, | ||||
|                   vertical: 12, | ||||
|                 ), | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border( | ||||
|                     bottom: BorderSide( | ||||
|                       color: theme.colorScheme.outline.withOpacity(0.2), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   const Gap(4), | ||||
|                   Text( | ||||
|                     'postCompose'.tr(), | ||||
|                     style: theme.textTheme.titleMedium!.copyWith(fontSize: 18), | ||||
|                   ), | ||||
|                   const Spacer(), | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Symbols.settings), | ||||
|                     onPressed: showSettingsSheet, | ||||
|                     tooltip: 'postSettings'.tr(), | ||||
|                     visualDensity: const VisualDensity( | ||||
|                       horizontal: -4, | ||||
|                       vertical: -2, | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     const Gap(4), | ||||
|                     Text( | ||||
|                       'postCompose'.tr(), | ||||
|                       style: theme.textTheme.titleMedium!.copyWith( | ||||
|                         fontSize: 18, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     onPressed: | ||||
|                         (state.submitting.value || | ||||
|                                 state.currentPublisher.value == null) | ||||
|                             ? null | ||||
|                             : performSubmit, | ||||
|                     icon: | ||||
|                         state.submitting.value | ||||
|                             ? SizedBox( | ||||
|                               width: 24, | ||||
|                               height: 24, | ||||
|                               child: const CircularProgressIndicator( | ||||
|                                 strokeWidth: 2, | ||||
|                               ), | ||||
|                             ) | ||||
|                             : Icon( | ||||
|                               originalPost != null | ||||
|                                   ? Symbols.edit | ||||
|                                   : Symbols.upload, | ||||
|                             ), | ||||
|                     tooltip: | ||||
|                         originalPost != null | ||||
|                             ? 'postUpdate'.tr() | ||||
|                             : 'postPublish'.tr(), | ||||
|                     visualDensity: const VisualDensity( | ||||
|                       horizontal: -4, | ||||
|                       vertical: -2, | ||||
|                     ), | ||||
|                   ), | ||||
|                   if (onCancel != null) | ||||
|                     const Spacer(), | ||||
|                     IconButton( | ||||
|                       icon: const Icon(Symbols.close), | ||||
|                       onPressed: onCancel, | ||||
|                       tooltip: 'cancel'.tr(), | ||||
|                       icon: const Icon(Symbols.settings), | ||||
|                       onPressed: showSettingsSheet, | ||||
|                       tooltip: 'postSettings'.tr(), | ||||
|                       visualDensity: const VisualDensity( | ||||
|                         horizontal: -4, | ||||
|                         vertical: -2, | ||||
|                       ), | ||||
|                     ), | ||||
|                 ], | ||||
|                     IconButton( | ||||
|                       onPressed: | ||||
|                           (state.submitting.value || | ||||
|                                   state.currentPublisher.value == null) | ||||
|                               ? null | ||||
|                               : performSubmit, | ||||
|                       icon: | ||||
|                           state.submitting.value | ||||
|                               ? SizedBox( | ||||
|                                 width: 24, | ||||
|                                 height: 24, | ||||
|                                 child: const CircularProgressIndicator( | ||||
|                                   strokeWidth: 2, | ||||
|                                 ), | ||||
|                               ) | ||||
|                               : Icon( | ||||
|                                 originalPost != null | ||||
|                                     ? Symbols.edit | ||||
|                                     : Symbols.upload, | ||||
|                               ), | ||||
|                       tooltip: | ||||
|                           originalPost != null | ||||
|                               ? 'postUpdate'.tr() | ||||
|                               : 'postPublish'.tr(), | ||||
|                       visualDensity: const VisualDensity( | ||||
|                         horizontal: -4, | ||||
|                         vertical: -2, | ||||
|                       ), | ||||
|                     ), | ||||
|                     if (onCancel != null) | ||||
|                       IconButton( | ||||
|                         icon: const Icon(Symbols.close), | ||||
|                         onPressed: onCancel, | ||||
|                         tooltip: 'cancel'.tr(), | ||||
|                         visualDensity: const VisualDensity( | ||||
|                           horizontal: -4, | ||||
|                           vertical: -2, | ||||
|                         ), | ||||
|                       ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|  | ||||
|             // Info banner (reply/forward) | ||||
|             ComposeInfoBanner( | ||||
| @@ -310,7 +316,7 @@ class PostComposeCard extends HookConsumerWidget { | ||||
|                           onTap: () { | ||||
|                             if (state.currentPublisher.value == null) { | ||||
|                               // No publisher loaded, guide user to create one | ||||
|                               if (isDialog) { | ||||
|                               if (isContained) { | ||||
|                                 Navigator.of(context).pop(); | ||||
|                               } | ||||
|                               context.pushNamed('creatorNew').then((value) { | ||||
| @@ -347,7 +353,7 @@ class PostComposeCard extends HookConsumerWidget { | ||||
|                                 onPublisherTap: () { | ||||
|                                   if (state.currentPublisher.value == null) { | ||||
|                                     // No publisher loaded, guide user to create one | ||||
|                                     if (isDialog) { | ||||
|                                     if (isContained) { | ||||
|                                       Navigator.of(context).pop(); | ||||
|                                     } | ||||
|                                     context.pushNamed('creatorNew').then(( | ||||
|   | ||||
| @@ -7,8 +7,13 @@ import 'package:island/models/post.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/services/event_bus.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/post/compose_card.dart'; | ||||
| import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||
| import 'package:island/widgets/post/compose_shared.dart'; | ||||
| import 'package:island/widgets/post/compose_state_utils.dart'; | ||||
| import 'package:island/widgets/post/compose_submit_utils.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| /// A dialog that wraps PostComposeCard for easy use in dialogs. | ||||
| /// This provides a convenient way to show the compose interface in a modal dialog. | ||||
| @@ -29,16 +34,15 @@ class PostComposeDialog extends HookConsumerWidget { | ||||
|     SnPost? originalPost, | ||||
|     PostComposeInitialState? initialState, | ||||
|   }) { | ||||
|     return showDialog<bool>( | ||||
|     return showModalBottomSheet<bool>( | ||||
|       context: context, | ||||
|       isScrollControlled: true, | ||||
|       useRootNavigator: true, | ||||
|       builder: | ||||
|           (context) => Padding( | ||||
|             padding: EdgeInsets.all(16), | ||||
|             child: PostComposeDialog( | ||||
|               originalPost: originalPost, | ||||
|               initialState: initialState, | ||||
|             ), | ||||
|           (context) => PostComposeDialog( | ||||
|             originalPost: originalPost, | ||||
|             initialState: initialState, | ||||
|             isBottomSheet: true, | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| @@ -48,7 +52,41 @@ class PostComposeDialog extends HookConsumerWidget { | ||||
|     final drafts = ref.watch(composeStorageNotifierProvider); | ||||
|     final restoredInitialState = useState<PostComposeInitialState?>(null); | ||||
|     final prompted = useState(false); | ||||
|     final isWide = isWideScreen(context); | ||||
|  | ||||
|     final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost; | ||||
|     final forwardedPost = | ||||
|         initialState?.forwardingTo ?? originalPost?.forwardedPost; | ||||
|  | ||||
|     // Create compose state | ||||
|     final state = useMemoized( | ||||
|       () => ComposeLogic.createState( | ||||
|         originalPost: originalPost, | ||||
|         forwardedPost: forwardedPost, | ||||
|         repliedPost: repliedPost, | ||||
|         postType: 0, | ||||
|       ), | ||||
|       [originalPost, forwardedPost, repliedPost], | ||||
|     ); | ||||
|  | ||||
|     // Add a listener to the entire state to trigger rebuilds | ||||
|     final stateNotifier = useMemoized( | ||||
|       () => Listenable.merge([ | ||||
|         state.titleController, | ||||
|         state.descriptionController, | ||||
|         state.contentController, | ||||
|         state.visibility, | ||||
|         state.attachments, | ||||
|         state.attachmentProgress, | ||||
|         state.currentPublisher, | ||||
|         state.submitting, | ||||
|       ]), | ||||
|       [state], | ||||
|     ); | ||||
|     useListenable(stateNotifier); | ||||
|  | ||||
|     // Use shared state management utilities | ||||
|     ComposeStateUtils.usePublisherInitialization(ref, state); | ||||
|     ComposeStateUtils.useInitialStateLoader(state, initialState); | ||||
|  | ||||
|     useEffect(() { | ||||
|       if (!prompted.value && | ||||
| @@ -64,24 +102,69 @@ class PostComposeDialog extends HookConsumerWidget { | ||||
|       return null; | ||||
|     }, [drafts, prompted.value]); | ||||
|  | ||||
|     return Dialog( | ||||
|       insetPadding: isWide ? const EdgeInsets.all(16) : EdgeInsets.zero, | ||||
|       child: ConstrainedBox( | ||||
|         constraints: | ||||
|             isWide | ||||
|                 ? const BoxConstraints(maxWidth: 600) | ||||
|                 : const BoxConstraints.expand(), | ||||
|         child: PostComposeCard( | ||||
|           originalPost: originalPost, | ||||
|           initialState: restoredInitialState.value ?? initialState, | ||||
|           onCancel: () => Navigator.of(context).pop(), | ||||
|           onSubmit: () { | ||||
|             // Fire event to notify listeners that a post was created | ||||
|             eventBus.fire(PostCreatedEvent()); | ||||
|             Navigator.of(context).pop(true); | ||||
|           }, | ||||
|           isDialog: true, | ||||
|         ), | ||||
|     // Helper methods for actions | ||||
|     void showSettingsSheet() { | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         useRootNavigator: true, | ||||
|         builder: (context) => ComposeSettingsSheet(state: state), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Future<void> performSubmit() async { | ||||
|       await ComposeSubmitUtils.performSubmit( | ||||
|         ref, | ||||
|         state, | ||||
|         context, | ||||
|         originalPost: originalPost, | ||||
|         repliedPost: repliedPost, | ||||
|         forwardedPost: forwardedPost, | ||||
|         onSuccess: () { | ||||
|           // Fire event to notify listeners that a post was created | ||||
|           eventBus.fire(PostCreatedEvent()); | ||||
|           Navigator.of(context).pop(true); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final actions = [ | ||||
|       IconButton( | ||||
|         icon: const Icon(Symbols.settings), | ||||
|         onPressed: showSettingsSheet, | ||||
|         tooltip: 'postSettings'.tr(), | ||||
|       ), | ||||
|       IconButton( | ||||
|         onPressed: | ||||
|             (state.submitting.value || state.currentPublisher.value == null) | ||||
|                 ? null | ||||
|                 : performSubmit, | ||||
|         icon: | ||||
|             state.submitting.value | ||||
|                 ? SizedBox( | ||||
|                   width: 24, | ||||
|                   height: 24, | ||||
|                   child: const CircularProgressIndicator(strokeWidth: 2), | ||||
|                 ) | ||||
|                 : Icon(originalPost != null ? Symbols.edit : Symbols.upload), | ||||
|         tooltip: originalPost != null ? 'postUpdate'.tr() : 'postPublish'.tr(), | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     return SheetScaffold( | ||||
|       titleText: 'postCompose'.tr(), | ||||
|       actions: actions, | ||||
|       child: PostComposeCard( | ||||
|         originalPost: originalPost, | ||||
|         initialState: restoredInitialState.value ?? initialState, | ||||
|         onCancel: () => Navigator.of(context).pop(), | ||||
|         onSubmit: () { | ||||
|           // Fire event to notify listeners that a post was created | ||||
|           eventBus.fire(PostCreatedEvent()); | ||||
|           Navigator.of(context).pop(true); | ||||
|         }, | ||||
|         isContained: true, | ||||
|         showHeader: false, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -157,6 +157,8 @@ class ComposeSettingsSheet extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final tagInputController = useTextEditingController(); | ||||
|  | ||||
|     return SheetScaffold( | ||||
|       titleText: 'postSettings'.tr(), | ||||
|       heightFactor: 0.6, | ||||
| @@ -255,6 +257,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { | ||||
|                     ), | ||||
|                   // Tag input with autocomplete | ||||
|                   TypeAheadField<SnPostTag>( | ||||
|                     controller: tagInputController, | ||||
|                     builder: (context, controller, focusNode) { | ||||
|                       return TextField( | ||||
|                         controller: controller, | ||||
| @@ -279,6 +282,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { | ||||
|                           borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                         ), | ||||
|                         title: Text('#${suggestion.slug}'), | ||||
|                         subtitle: Text('${suggestion.usage} posts'), | ||||
|                         dense: true, | ||||
|                       ); | ||||
|                     }, | ||||
| @@ -289,6 +293,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { | ||||
|                           suggestion.slug, | ||||
|                         ]; | ||||
|                       } | ||||
|                       tagInputController.clear(); | ||||
|                     }, | ||||
|                     direction: VerticalDirection.down, | ||||
|                     hideOnEmpty: true, | ||||
|   | ||||
							
								
								
									
										300
									
								
								lib/widgets/post/compose_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								lib/widgets/post/compose_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/services/event_bus.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/post/compose_card.dart'; | ||||
| import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||
| import 'package:island/widgets/post/compose_shared.dart'; | ||||
| import 'package:island/widgets/post/compose_state_utils.dart'; | ||||
| import 'package:island/widgets/post/compose_submit_utils.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| /// A dialog that wraps PostComposeCard for easy use in dialogs. | ||||
| /// This provides a convenient way to show the compose interface in a modal dialog. | ||||
| class PostComposeSheet extends HookConsumerWidget { | ||||
|   final SnPost? originalPost; | ||||
|   final PostComposeInitialState? initialState; | ||||
|   final bool isBottomSheet; | ||||
|  | ||||
|   const PostComposeSheet({ | ||||
|     super.key, | ||||
|     this.originalPost, | ||||
|     this.initialState, | ||||
|     this.isBottomSheet = false, | ||||
|   }); | ||||
|  | ||||
|   static Future<bool?> show( | ||||
|     BuildContext context, { | ||||
|     SnPost? originalPost, | ||||
|     PostComposeInitialState? initialState, | ||||
|   }) { | ||||
|     return showModalBottomSheet<bool>( | ||||
|       context: context, | ||||
|       isScrollControlled: true, | ||||
|       useRootNavigator: true, | ||||
|       builder: | ||||
|           (context) => PostComposeSheet( | ||||
|             originalPost: originalPost, | ||||
|             initialState: initialState, | ||||
|             isBottomSheet: true, | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final drafts = ref.watch(composeStorageNotifierProvider); | ||||
|     final restoredInitialState = useState<PostComposeInitialState?>(null); | ||||
|     final prompted = useState(false); | ||||
|  | ||||
|     final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost; | ||||
|     final forwardedPost = | ||||
|         initialState?.forwardingTo ?? originalPost?.forwardedPost; | ||||
|  | ||||
|     // Create compose state | ||||
|     final state = useMemoized( | ||||
|       () => ComposeLogic.createState( | ||||
|         originalPost: originalPost, | ||||
|         forwardedPost: forwardedPost, | ||||
|         repliedPost: repliedPost, | ||||
|         postType: 0, | ||||
|       ), | ||||
|       [originalPost, forwardedPost, repliedPost], | ||||
|     ); | ||||
|  | ||||
|     // Add a listener to the entire state to trigger rebuilds | ||||
|     final stateNotifier = useMemoized( | ||||
|       () => Listenable.merge([ | ||||
|         state.titleController, | ||||
|         state.descriptionController, | ||||
|         state.contentController, | ||||
|         state.visibility, | ||||
|         state.attachments, | ||||
|         state.attachmentProgress, | ||||
|         state.currentPublisher, | ||||
|         state.submitting, | ||||
|       ]), | ||||
|       [state], | ||||
|     ); | ||||
|     useListenable(stateNotifier); | ||||
|  | ||||
|     // Use shared state management utilities | ||||
|     ComposeStateUtils.usePublisherInitialization(ref, state); | ||||
|     ComposeStateUtils.useInitialStateLoader(state, initialState); | ||||
|  | ||||
|     useEffect(() { | ||||
|       if (!prompted.value && | ||||
|           originalPost == null && | ||||
|           initialState?.replyingTo == null && | ||||
|           initialState?.forwardingTo == null && | ||||
|           drafts.isNotEmpty) { | ||||
|         prompted.value = true; | ||||
|         WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|           _showRestoreDialog(ref, restoredInitialState); | ||||
|         }); | ||||
|       } | ||||
|       return null; | ||||
|     }, [drafts, prompted.value]); | ||||
|  | ||||
|     // Helper methods for actions | ||||
|     void showSettingsSheet() { | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         useRootNavigator: true, | ||||
|         builder: (context) => ComposeSettingsSheet(state: state), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Future<void> performSubmit() async { | ||||
|       await ComposeSubmitUtils.performSubmit( | ||||
|         ref, | ||||
|         state, | ||||
|         context, | ||||
|         originalPost: originalPost, | ||||
|         repliedPost: repliedPost, | ||||
|         forwardedPost: forwardedPost, | ||||
|         onSuccess: () { | ||||
|           // Fire event to notify listeners that a post was created | ||||
|           eventBus.fire(PostCreatedEvent()); | ||||
|           Navigator.of(context).pop(true); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final actions = [ | ||||
|       IconButton( | ||||
|         icon: const Icon(Symbols.settings), | ||||
|         onPressed: showSettingsSheet, | ||||
|         tooltip: 'postSettings'.tr(), | ||||
|       ), | ||||
|       IconButton( | ||||
|         onPressed: | ||||
|             (state.submitting.value || state.currentPublisher.value == null) | ||||
|                 ? null | ||||
|                 : performSubmit, | ||||
|         icon: | ||||
|             state.submitting.value | ||||
|                 ? SizedBox( | ||||
|                   width: 24, | ||||
|                   height: 24, | ||||
|                   child: const CircularProgressIndicator(strokeWidth: 2), | ||||
|                 ) | ||||
|                 : Icon(originalPost != null ? Symbols.edit : Symbols.upload), | ||||
|         tooltip: originalPost != null ? 'postUpdate'.tr() : 'postPublish'.tr(), | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     return SheetScaffold( | ||||
|       titleText: 'postCompose'.tr(), | ||||
|       actions: actions, | ||||
|       child: PostComposeCard( | ||||
|         originalPost: originalPost, | ||||
|         initialState: restoredInitialState.value ?? initialState, | ||||
|         onCancel: () => Navigator.of(context).pop(), | ||||
|         onSubmit: () { | ||||
|           // Fire event to notify listeners that a post was created | ||||
|           eventBus.fire(PostCreatedEvent()); | ||||
|           Navigator.of(context).pop(true); | ||||
|         }, | ||||
|         isContained: true, | ||||
|         showHeader: false, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> _showRestoreDialog( | ||||
|     WidgetRef ref, | ||||
|     ValueNotifier<PostComposeInitialState?> restoredInitialState, | ||||
|   ) async { | ||||
|     final drafts = ref.read(composeStorageNotifierProvider); | ||||
|     if (drafts.isNotEmpty) { | ||||
|       final latestDraft = drafts.values.last; | ||||
|  | ||||
|       final restore = await showDialog<bool>( | ||||
|         context: ref.context, | ||||
|         useRootNavigator: true, | ||||
|         builder: | ||||
|             (context) => AlertDialog( | ||||
|               title: Text('restoreDraftTitle'.tr()), | ||||
|               content: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   Text('restoreDraftMessage'.tr()), | ||||
|                   const SizedBox(height: 16), | ||||
|                   _buildCompactDraftPreview(context, latestDraft), | ||||
|                 ], | ||||
|               ), | ||||
|               actions: [ | ||||
|                 TextButton( | ||||
|                   onPressed: () => Navigator.of(context).pop(false), | ||||
|                   child: Text('no'.tr()), | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   onPressed: () => Navigator.of(context).pop(true), | ||||
|                   child: Text('yes'.tr()), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|       ); | ||||
|       if (restore == true) { | ||||
|         // Delete the old draft | ||||
|         await ref | ||||
|             .read(composeStorageNotifierProvider.notifier) | ||||
|             .deleteDraft(latestDraft.id); | ||||
|         restoredInitialState.value = PostComposeInitialState( | ||||
|           title: latestDraft.title, | ||||
|           description: latestDraft.description, | ||||
|           content: latestDraft.content, | ||||
|           visibility: latestDraft.visibility, | ||||
|           attachments: | ||||
|               latestDraft.attachments | ||||
|                   .map((e) => UniversalFile.fromAttachment(e)) | ||||
|                   .toList(), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildCompactDraftPreview(BuildContext context, SnPost draft) { | ||||
|     return Container( | ||||
|       padding: const EdgeInsets.all(12), | ||||
|       decoration: BoxDecoration( | ||||
|         color: Theme.of(context).colorScheme.surface, | ||||
|         borderRadius: BorderRadius.circular(8), | ||||
|         border: Border.all( | ||||
|           color: Theme.of(context).colorScheme.outline.withOpacity(0.3), | ||||
|         ), | ||||
|       ), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             children: [ | ||||
|               Icon( | ||||
|                 Icons.description, | ||||
|                 size: 16, | ||||
|                 color: Theme.of(context).colorScheme.primary, | ||||
|               ), | ||||
|               const SizedBox(width: 8), | ||||
|               Text( | ||||
|                 'draft'.tr(), | ||||
|                 style: Theme.of(context).textTheme.labelMedium?.copyWith( | ||||
|                   color: Theme.of(context).colorScheme.primary, | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           const SizedBox(height: 8), | ||||
|           if (draft.title?.isNotEmpty ?? false) | ||||
|             Text( | ||||
|               draft.title!, | ||||
|               style: TextStyle( | ||||
|                 fontWeight: FontWeight.w500, | ||||
|                 fontSize: 14, | ||||
|                 color: Theme.of(context).colorScheme.onSurface, | ||||
|               ), | ||||
|               maxLines: 1, | ||||
|               overflow: TextOverflow.ellipsis, | ||||
|             ), | ||||
|           if (draft.content?.isNotEmpty ?? false) | ||||
|             Text( | ||||
|               draft.content!, | ||||
|               style: TextStyle( | ||||
|                 fontSize: 12, | ||||
|                 color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|               ), | ||||
|               maxLines: 2, | ||||
|               overflow: TextOverflow.ellipsis, | ||||
|             ), | ||||
|           if (draft.attachments.isNotEmpty) | ||||
|             Row( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               children: [ | ||||
|                 Icon( | ||||
|                   Icons.attach_file, | ||||
|                   size: 12, | ||||
|                   color: Theme.of(context).colorScheme.secondary, | ||||
|                 ), | ||||
|                 const SizedBox(width: 4), | ||||
|                 Text( | ||||
|                   '${draft.attachments.length} attachment${draft.attachments.length > 1 ? 's' : ''}', | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).colorScheme.secondary, | ||||
|                     fontSize: 11, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -178,10 +178,9 @@ class ComposeToolbar extends HookConsumerWidget { | ||||
|                       horizontal: -4, | ||||
|                       vertical: -4, | ||||
|                     ), | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     constraints: const BoxConstraints( | ||||
|                       minWidth: 32, | ||||
|                       minHeight: 32, | ||||
|                       minHeight: 48, | ||||
|                     ), | ||||
|                   ) | ||||
|                 else if (originalPost == null) | ||||
| @@ -195,10 +194,9 @@ class ComposeToolbar extends HookConsumerWidget { | ||||
|                       horizontal: -4, | ||||
|                       vertical: -4, | ||||
|                     ), | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     constraints: const BoxConstraints( | ||||
|                       minWidth: 32, | ||||
|                       minHeight: 32, | ||||
|                       minHeight: 48, | ||||
|                     ), | ||||
|                   ), | ||||
|               ], | ||||
|   | ||||
| @@ -26,7 +26,7 @@ import 'package:island/widgets/post/embed_view_renderer.dart'; | ||||
| import 'package:island/widgets/post/post_reaction_sheet.dart'; | ||||
| import 'package:island/widgets/safety/abuse_report_helper.dart'; | ||||
| import 'package:island/widgets/share/share_sheet.dart'; | ||||
| import 'package:island/widgets/post/compose_dialog.dart'; | ||||
| import 'package:island/widgets/post/compose_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:path_provider/path_provider.dart' show getTemporaryDirectory; | ||||
| import 'package:screenshot/screenshot.dart'; | ||||
| @@ -180,7 +180,7 @@ class PostActionableItem extends HookConsumerWidget { | ||||
|                 title: 'edit'.tr(), | ||||
|                 image: MenuImage.icon(Symbols.edit), | ||||
|                 callback: () async { | ||||
|                   final result = await PostComposeDialog.show( | ||||
|                   final result = await PostComposeSheet.show( | ||||
|                     context, | ||||
|                     originalPost: item, | ||||
|                   ); | ||||
| @@ -227,7 +227,7 @@ class PostActionableItem extends HookConsumerWidget { | ||||
|               title: 'reply'.tr(), | ||||
|               image: MenuImage.icon(Symbols.reply), | ||||
|               callback: () async { | ||||
|                 final result = await PostComposeDialog.show( | ||||
|                 final result = await PostComposeSheet.show( | ||||
|                   context, | ||||
|                   initialState: PostComposeInitialState(replyingTo: item), | ||||
|                 ); | ||||
| @@ -240,7 +240,7 @@ class PostActionableItem extends HookConsumerWidget { | ||||
|               title: 'forward'.tr(), | ||||
|               image: MenuImage.icon(Symbols.forward), | ||||
|               callback: () async { | ||||
|                 final result = await PostComposeDialog.show( | ||||
|                 final result = await PostComposeSheet.show( | ||||
|                   context, | ||||
|                   initialState: PostComposeInitialState(forwardingTo: item), | ||||
|                 ); | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import 'package:island/screens/creators/publishers_form.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/post/compose_dialog.dart'; | ||||
| import 'package:island/widgets/post/compose_sheet.dart'; | ||||
| import 'package:island/widgets/post/publishers_modal.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| @@ -124,7 +124,7 @@ class PostQuickReply extends HookConsumerWidget { | ||||
|                   IconButton( | ||||
|                     onPressed: () async { | ||||
|                       onLaunch?.call(); | ||||
|                       final value = await PostComposeDialog.show( | ||||
|                       final value = await PostComposeSheet.show( | ||||
|                         context, | ||||
|                         initialState: PostComposeInitialState( | ||||
|                           content: contentController.text, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import 'package:gap/gap.dart'; | ||||
| import 'package:island/models/thought.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/post/compose_dialog.dart'; | ||||
| import 'package:island/widgets/post/compose_sheet.dart'; | ||||
| import 'package:island/widgets/thought/function_calls_section.dart'; | ||||
| import 'package:island/widgets/thought/proposals_section.dart'; | ||||
| import 'package:island/widgets/thought/reasoning_section.dart'; | ||||
| @@ -29,7 +29,7 @@ void _handleProposalAction(BuildContext context, Map<String, String> proposal) { | ||||
|   switch (proposal['type']) { | ||||
|     case 'post_create': | ||||
|       // Show post creation dialog with the proposal content | ||||
|       PostComposeDialog.show( | ||||
|       PostComposeSheet.show( | ||||
|         context, | ||||
|         initialState: PostComposeInitialState( | ||||
|           content: (proposal['content'] ?? '').trim(), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user