305 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
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 ComposeState 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]);
 | 
						|
 | 
						|
    // Dispose state when widget is disposed
 | 
						|
    useEffect(() => () => ComposeLogic.dispose(state), []);
 | 
						|
 | 
						|
    // 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,
 | 
						|
        providedState: state,
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  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,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |