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.tagsController.clearTags();
|
|
|
|
// 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
|
|
}
|
|
}
|