import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/account.dart'; import 'package:island/models/autocomplete_response.dart'; import 'package:island/models/chat.dart'; import 'package:island/models/publisher.dart'; import 'package:island/models/realm.dart'; import 'package:island/models/sticker.dart'; import 'package:island/services/autocomplete_service.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/compose_shared.dart'; /// A reusable widget for the form fields in compose screens. /// Includes title, description, and content text fields. class ComposeFormFields extends HookConsumerWidget { final ComposeState state; final bool enabled; final bool showPublisherAvatar; final VoidCallback? onPublisherTap; const ComposeFormFields({ super.key, required this.state, this.enabled = true, this.showPublisherAvatar = true, this.onPublisherTap, }); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Row( spacing: 12, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Publisher profile picture if (showPublisherAvatar) GestureDetector( onTap: onPublisherTap, child: ProfilePictureWidget( fileId: state.currentPublisher.value?.picture?.id, radius: 20, fallbackIcon: state.currentPublisher.value == null ? Icons.question_mark : null, ), ), // Post content form Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (state.currentPublisher.value == null) Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.info, size: 16, color: theme.colorScheme.primary, ), const SizedBox(width: 8), Expanded( child: Text( 'Tap the avatar to create a publisher and start composing.', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ], ), ), // Title field TextField( controller: state.titleController, enabled: enabled && state.currentPublisher.value != null, decoration: InputDecoration( hintText: 'postTitle'.tr(), border: InputBorder.none, isCollapsed: true, contentPadding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8, ), ), style: theme.textTheme.titleMedium, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), // Description field TextField( controller: state.descriptionController, enabled: enabled && state.currentPublisher.value != null, decoration: InputDecoration( hintText: 'postDescription'.tr(), border: InputBorder.none, isCollapsed: true, contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12), ), style: theme.textTheme.bodyMedium, minLines: 1, maxLines: 3, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), // Content field TypeAheadField( controller: state.contentController, builder: (context, controller, focusNode) { return TextField( focusNode: focusNode, controller: controller, enabled: enabled && state.currentPublisher.value != null, style: theme.textTheme.bodyMedium, decoration: InputDecoration( border: InputBorder.none, hintText: 'postContent'.tr(), isCollapsed: true, contentPadding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8, ), ), maxLines: null, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ); }, suggestionsCallback: (pattern) async { // Only trigger on @ or : final atIndex = pattern.lastIndexOf('@'); final colonIndex = pattern.lastIndexOf(':'); final triggerIndex = atIndex > colonIndex ? atIndex : colonIndex; if (triggerIndex == -1) return []; final service = ref.read(autocompleteServiceProvider); try { return await service.getGeneralSuggestions(pattern); } catch (e) { return []; } }, itemBuilder: (context, suggestion) { String title = 'unknown'.tr(); Widget leading = Icon(Icons.help); switch (suggestion.type) { case 'user': final user = SnAccount.fromJson(suggestion.data); title = user.nick; leading = ProfilePictureWidget( file: user.profile.picture, radius: 18, ); break; case 'chatroom': final chatRoom = SnChatRoom.fromJson(suggestion.data); title = chatRoom.name ?? 'Chat Room'; leading = ProfilePictureWidget( file: chatRoom.picture, radius: 18, ); break; case 'realm': final realm = SnRealm.fromJson(suggestion.data); title = realm.name; leading = ProfilePictureWidget( file: realm.picture, radius: 18, ); break; case 'publisher': final publisher = SnPublisher.fromJson(suggestion.data); title = publisher.name; leading = ProfilePictureWidget( file: publisher.picture, radius: 18, ); break; case 'sticker': final sticker = SnSticker.fromJson(suggestion.data); title = sticker.slug; leading = ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 28, height: 28, child: CloudImageWidget(fileId: sticker.imageId), ), ); break; default: } return ListTile( leading: leading, title: Text(title), subtitle: Text(suggestion.keyword), dense: true, ); }, onSelected: (suggestion) { final text = state.contentController.text; final atIndex = text.lastIndexOf('@'); final colonIndex = text.lastIndexOf(':'); final triggerIndex = atIndex > colonIndex ? atIndex : colonIndex; if (triggerIndex == -1) return; final newText = text.replaceRange( triggerIndex, text.length, suggestion.keyword, ); state.contentController.value = TextEditingValue( text: newText, selection: TextSelection.collapsed( offset: triggerIndex + suggestion.keyword.length, ), ); }, direction: VerticalDirection.down, hideOnEmpty: true, hideOnLoading: true, debounceDuration: const Duration(milliseconds: 500), loadingBuilder: (context) => const Text('Loading...'), errorBuilder: (context, error) => const Text('Error!'), emptyBuilder: (context) => const Text('No items found!'), ), ], ), ), ], ); } } /// A specialized form fields widget for article compose with expanded content field. class ArticleComposeFormFields extends StatelessWidget { final ComposeState state; final bool enabled; const ArticleComposeFormFields({ super.key, required this.state, this.enabled = true, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title field TextField( controller: state.titleController, decoration: InputDecoration( hintText: 'postTitle', border: InputBorder.none, isCollapsed: true, contentPadding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8, ), ), style: theme.textTheme.titleMedium, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), // Description field TextField( controller: state.descriptionController, decoration: InputDecoration( hintText: 'postDescription', border: InputBorder.none, isCollapsed: true, contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12), ), style: theme.textTheme.bodyMedium, minLines: 1, maxLines: 3, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), // Content field (expanded) Expanded( child: TextField( controller: state.contentController, style: theme.textTheme.bodyMedium, decoration: InputDecoration( border: InputBorder.none, hintText: 'postContent', contentPadding: const EdgeInsets.symmetric( vertical: 16, horizontal: 8, ), ), maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), ), ], ), ), ); } }