201 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			201 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.dart';
 | |
| import 'package:gap/gap.dart';
 | |
| import 'package:go_router/go_router.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/models/poll.dart';
 | |
| import 'package:island/models/publisher.dart';
 | |
| import 'package:island/screens/creators/poll/poll_list.dart';
 | |
| import 'package:island/widgets/content/sheet.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| import 'package:island/widgets/post/publishers_modal.dart';
 | |
| 
 | |
| /// Bottom sheet for selecting or creating a poll. Returns SnPoll via Navigator.pop.
 | |
| class ComposePollSheet extends HookConsumerWidget {
 | |
|   /// Optional publisher name to filter polls and prefill creation.
 | |
|   final String? pubName;
 | |
| 
 | |
|   const ComposePollSheet({super.key, this.pubName});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final selectedPublisher = useState<String?>(pubName);
 | |
|     final isPushing = useState(false);
 | |
|     final errorText = useState<String?>(null);
 | |
| 
 | |
|     return SheetScaffold(
 | |
|       heightFactor: 0.6,
 | |
|       titleText: 'poll'.tr(),
 | |
|       child: DefaultTabController(
 | |
|         length: 2,
 | |
|         child: Column(
 | |
|           mainAxisSize: MainAxisSize.min,
 | |
|           crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|           children: [
 | |
|             TabBar(
 | |
|               tabs: [
 | |
|                 Tab(text: 'pollsRecent'.tr()),
 | |
|                 Tab(text: 'pollCreateNew'.tr()),
 | |
|               ],
 | |
|             ),
 | |
|             Expanded(
 | |
|               child: TabBarView(
 | |
|                 children: [
 | |
|                   // Link/Select existing poll list
 | |
|                   PagingHelperView(
 | |
|                     provider: pollListNotifierProvider(pubName),
 | |
|                     futureRefreshable: pollListNotifierProvider(pubName).future,
 | |
|                     notifierRefreshable:
 | |
|                         pollListNotifierProvider(pubName).notifier,
 | |
|                     contentBuilder:
 | |
|                         (data, widgetCount, endItemView) => ListView.builder(
 | |
|                           padding: EdgeInsets.zero,
 | |
|                           itemCount: widgetCount,
 | |
|                           itemBuilder: (context, index) {
 | |
|                             if (index == widgetCount - 1) {
 | |
|                               return endItemView;
 | |
|                             }
 | |
| 
 | |
|                             final poll = data.items[index];
 | |
| 
 | |
|                             return ListTile(
 | |
|                               leading: const Icon(Symbols.how_to_vote, fill: 1),
 | |
|                               title: Text(poll.title ?? 'untitled'.tr()),
 | |
|                               subtitle: _buildPollSubtitle(poll),
 | |
|                               onTap: () {
 | |
|                                 Navigator.of(context).pop(poll);
 | |
|                               },
 | |
|                             );
 | |
|                           },
 | |
|                         ),
 | |
|                   ),
 | |
| 
 | |
|                   // Create new poll and return it
 | |
|                   SingleChildScrollView(
 | |
|                     child: Column(
 | |
|                       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                       children: [
 | |
|                         Text(
 | |
|                           'pollCreateNewHint',
 | |
|                         ).tr().fontSize(13).opacity(0.85).padding(bottom: 8),
 | |
|                         ListTile(
 | |
|                           title: Text(
 | |
|                             selectedPublisher.value == null
 | |
|                                 ? 'publisher'.tr()
 | |
|                                 : '@${selectedPublisher.value}',
 | |
|                           ),
 | |
|                           subtitle: Text(
 | |
|                             selectedPublisher.value == null
 | |
|                                 ? 'publisherHint'.tr()
 | |
|                                 : 'selected'.tr(),
 | |
|                           ),
 | |
|                           leading: const Icon(Symbols.account_circle),
 | |
|                           trailing: const Icon(Symbols.chevron_right),
 | |
|                           onTap: () async {
 | |
|                             final picked =
 | |
|                                 await showModalBottomSheet<SnPublisher>(
 | |
|                                   context: context,
 | |
|                                   isScrollControlled: true,
 | |
|                                   builder: (context) => const PublisherModal(),
 | |
|                                 );
 | |
|                             if (picked != null) {
 | |
|                               try {
 | |
|                                 final name = picked.name;
 | |
|                                 if (name.isNotEmpty) {
 | |
|                                   selectedPublisher.value = name;
 | |
|                                   errorText.value = null;
 | |
|                                 }
 | |
|                               } catch (_) {
 | |
|                                 // ignore
 | |
|                               }
 | |
|                             }
 | |
|                           },
 | |
|                         ),
 | |
|                         if (errorText.value != null)
 | |
|                           Padding(
 | |
|                             padding: const EdgeInsets.only(
 | |
|                               left: 16,
 | |
|                               right: 16,
 | |
|                               top: 4,
 | |
|                             ),
 | |
|                             child: Text(
 | |
|                               errorText.value!,
 | |
|                               style: TextStyle(color: Colors.red[700]),
 | |
|                             ),
 | |
|                           ),
 | |
|                         const Gap(16),
 | |
|                         Align(
 | |
|                           alignment: Alignment.centerRight,
 | |
|                           child: FilledButton.icon(
 | |
|                             icon:
 | |
|                                 isPushing.value
 | |
|                                     ? const SizedBox(
 | |
|                                       width: 18,
 | |
|                                       height: 18,
 | |
|                                       child: CircularProgressIndicator(
 | |
|                                         strokeWidth: 2,
 | |
|                                         color: Colors.white,
 | |
|                                       ),
 | |
|                                     )
 | |
|                                     : const Icon(Symbols.add_circle),
 | |
|                             label: Text('create'.tr()),
 | |
|                             onPressed:
 | |
|                                 isPushing.value
 | |
|                                     ? null
 | |
|                                     : () async {
 | |
|                                       final pub = selectedPublisher.value ?? '';
 | |
|                                       if (pub.isEmpty) {
 | |
|                                         errorText.value =
 | |
|                                             'publisherCannotBeEmpty'.tr();
 | |
|                                         return;
 | |
|                                       }
 | |
|                                       errorText.value = null;
 | |
| 
 | |
|                                       isPushing.value = true;
 | |
|                                       // Push to creatorPollNew route and await result
 | |
|                                       final result = await GoRouter.of(
 | |
|                                         context,
 | |
|                                       ).push<SnPoll>(
 | |
|                                         '/creators/$pub/polls/new',
 | |
|                                       );
 | |
| 
 | |
|                                       if (result == null) {
 | |
|                                         isPushing.value = false;
 | |
|                                         return;
 | |
|                                       }
 | |
| 
 | |
|                                       if (!context.mounted) return;
 | |
| 
 | |
|                                       // Return created poll to caller of this bottom sheet
 | |
|                                       Navigator.of(context).pop(result);
 | |
|                                     },
 | |
|                           ),
 | |
|                         ),
 | |
|                       ],
 | |
|                     ).padding(horizontal: 24, vertical: 24),
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget? _buildPollSubtitle(SnPollWithStats poll) {
 | |
|     try {
 | |
|       final List<SnPollQuestion> options = poll.questions;
 | |
|       if (options.isEmpty) return null;
 | |
|       final preview = options.take(3).map((e) => e.title).join(' · ');
 | |
|       if (preview.trim().isEmpty) return null;
 | |
|       return Text(preview);
 | |
|     } catch (_) {
 | |
|       return null;
 | |
|     }
 | |
|   }
 | |
| }
 |