195 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			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/creators/publishers_form.dart';
 | |
| import 'package:island/screens/posts/compose.dart';
 | |
| import 'package:island/services/compose_storage_db.dart';
 | |
| import 'package:island/widgets/post/compose_shared.dart';
 | |
| 
 | |
| /// Utility class for common compose state management logic.
 | |
| class ComposeStateUtils {
 | |
|   /// Initializes publisher when data becomes available.
 | |
|   static void usePublisherInitialization(WidgetRef ref, ComposeState state) {
 | |
|     final publishers = ref.watch(publishersManagedProvider);
 | |
| 
 | |
|     useEffect(() {
 | |
|       if (publishers.value?.isNotEmpty ?? false) {
 | |
|         if (state.currentPublisher.value == null) {
 | |
|           state.currentPublisher.value = publishers.value!.first;
 | |
|         }
 | |
|       }
 | |
|       return null;
 | |
|     }, [publishers]);
 | |
|   }
 | |
| 
 | |
|   /// Loads initial state from provided parameters.
 | |
|   static void useInitialStateLoader(
 | |
|     ComposeState state,
 | |
|     PostComposeInitialState? initialState,
 | |
|   ) {
 | |
|     useEffect(() {
 | |
|       if (initialState != null) {
 | |
|         state.titleController.text = initialState.title ?? '';
 | |
|         state.descriptionController.text = initialState.description ?? '';
 | |
|         state.contentController.text = initialState.content ?? '';
 | |
|         if (initialState.visibility != null) {
 | |
|           state.visibility.value = initialState.visibility!;
 | |
|         }
 | |
|         if (initialState.attachments.isNotEmpty) {
 | |
|           state.attachments.value = List.from(initialState.attachments);
 | |
|         }
 | |
|       }
 | |
|       return null;
 | |
|     }, [initialState]);
 | |
|   }
 | |
| 
 | |
|   /// Loads draft if available (for new posts without initial state).
 | |
|   static void useDraftLoader(
 | |
|     WidgetRef ref,
 | |
|     ComposeState state,
 | |
|     SnPost? originalPost,
 | |
|     SnPost? repliedPost,
 | |
|     SnPost? forwardedPost,
 | |
|     PostComposeInitialState? initialState,
 | |
|   ) {
 | |
|     useEffect(() {
 | |
|       if (originalPost == null &&
 | |
|           forwardedPost == null &&
 | |
|           repliedPost == null &&
 | |
|           initialState == null) {
 | |
|         // Try to load the most recent draft
 | |
|         final drafts = ref.read(composeStorageNotifierProvider);
 | |
|         if (drafts.isNotEmpty) {
 | |
|           final mostRecentDraft = drafts.values.reduce(
 | |
|             (a, b) =>
 | |
|                 (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0))
 | |
|                     ? a
 | |
|                     : b,
 | |
|           );
 | |
| 
 | |
|           // Only load if the draft has meaningful content
 | |
|           if (mostRecentDraft.content?.isNotEmpty == true ||
 | |
|               mostRecentDraft.title?.isNotEmpty == true) {
 | |
|             state.titleController.text = mostRecentDraft.title ?? '';
 | |
|             state.descriptionController.text =
 | |
|                 mostRecentDraft.description ?? '';
 | |
|             state.contentController.text = mostRecentDraft.content ?? '';
 | |
|             state.visibility.value = mostRecentDraft.visibility;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return null;
 | |
|     }, []);
 | |
|   }
 | |
| 
 | |
|   /// Handles auto-save functionality for new posts.
 | |
|   static void useAutoSave(WidgetRef ref, ComposeState state, bool isNewPost) {
 | |
|     useEffect(() {
 | |
|       if (isNewPost) {
 | |
|         state.startAutoSave(ref);
 | |
|       }
 | |
|       return () => state.stopAutoSave();
 | |
|     }, [state]);
 | |
|   }
 | |
| 
 | |
|   /// Handles disposal and draft saving logic.
 | |
|   static void useDisposalHandler(
 | |
|     WidgetRef ref,
 | |
|     ComposeState state,
 | |
|     SnPost? originalPost,
 | |
|     bool submitted,
 | |
|   ) {
 | |
|     useEffect(() {
 | |
|       return () {
 | |
|         if (!submitted &&
 | |
|             originalPost == null &&
 | |
|             state.currentPublisher.value != null) {
 | |
|           final hasContent =
 | |
|               state.titleController.text.trim().isNotEmpty ||
 | |
|               state.descriptionController.text.trim().isNotEmpty ||
 | |
|               state.contentController.text.trim().isNotEmpty;
 | |
|           final hasAttachments = state.attachments.value.isNotEmpty;
 | |
|           if (hasContent || hasAttachments) {
 | |
|             final draft = SnPost(
 | |
|               id: state.draftId,
 | |
|               title: state.titleController.text,
 | |
|               description: state.descriptionController.text,
 | |
|               content: state.contentController.text,
 | |
|               visibility: state.visibility.value,
 | |
|               type: state.postType,
 | |
|               attachments:
 | |
|                   state.attachments.value
 | |
|                       .where((e) => e.isOnCloud)
 | |
|                       .map((e) => e.data as SnCloudFile)
 | |
|                       .toList(),
 | |
|               publisher: state.currentPublisher.value!,
 | |
|               updatedAt: DateTime.now(),
 | |
|             );
 | |
|             ref
 | |
|                 .read(composeStorageNotifierProvider.notifier)
 | |
|                 .saveDraft(draft)
 | |
|                 .catchError((e) => debugPrint('Failed to save draft: $e'));
 | |
|           }
 | |
|         }
 | |
|         ComposeLogic.dispose(state);
 | |
|       };
 | |
|     }, []);
 | |
|   }
 | |
| 
 | |
|   /// Creates and manages the state notifier for rebuilds.
 | |
|   static Listenable useStateNotifier(ComposeState state) {
 | |
|     return useMemoized(
 | |
|       () => Listenable.merge([
 | |
|         state.titleController,
 | |
|         state.descriptionController,
 | |
|         state.contentController,
 | |
|         state.visibility,
 | |
|         state.attachments,
 | |
|         state.attachmentProgress,
 | |
|         state.currentPublisher,
 | |
|         state.submitting,
 | |
|       ]),
 | |
|       [state],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /// Resets form to clean state for new composition.
 | |
|   static void resetForm(ComposeState state) {
 | |
|     // Clear text fields
 | |
|     state.titleController.clear();
 | |
|     state.descriptionController.clear();
 | |
|     state.contentController.clear();
 | |
|     state.slugController.clear();
 | |
| 
 | |
|     // Reset visibility to default (0 = public)
 | |
|     state.visibility.value = 0;
 | |
| 
 | |
|     // Clear attachments
 | |
|     state.attachments.value = [];
 | |
| 
 | |
|     // Clear attachment progress
 | |
|     state.attachmentProgress.value = {};
 | |
| 
 | |
|     // Clear tags
 | |
|     state.tags.value = [];
 | |
| 
 | |
|     // Clear categories
 | |
|     state.categories.value = [];
 | |
| 
 | |
|     // Clear embed view
 | |
|     state.embedView.value = null;
 | |
| 
 | |
|     // Clear poll
 | |
|     state.pollId.value = null;
 | |
| 
 | |
|     // Clear realm
 | |
|     state.realm.value = null;
 | |
| 
 | |
|     // Generate new draft ID for fresh composition
 | |
|     // Note: We don't recreate the entire state, just reset the fields
 | |
|     // The existing state object is reused for continuity
 | |
|   }
 | |
| }
 |