diff --git a/lib/route.dart b/lib/route.dart index 7d9cc73e..d2e5b110 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -50,7 +50,6 @@ import 'package:island/screens/discovery/feeds/feed_detail.dart'; import 'package:island/screens/creators/poll/poll_list.dart'; import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/screens/creators/webfeed/webfeed_list.dart'; -import 'package:island/screens/creators/webfeed/webfeed_edit.dart'; import 'package:island/screens/poll/poll_editor.dart'; import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/post_detail.dart'; @@ -162,27 +161,6 @@ final routerProvider = Provider((ref) { final name = state.pathParameters['name']!; return WebFeedListScreen(pubName: name); }, - routes: [ - GoRoute( - name: 'creatorFeedNew', - path: 'new', - builder: (context, state) { - return WebFeedNewScreen( - pubName: state.pathParameters['name']!, - ); - }, - ), - GoRoute( - name: 'creatorFeedEdit', - path: ':feedId', - builder: (context, state) { - return WebFeedEditScreen( - pubName: state.pathParameters['name']!, - feedId: state.pathParameters['feedId'], - ); - }, - ), - ], ), GoRoute( name: 'creatorPosts', diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 6c54acf5..cf4b3cfc 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -175,7 +175,7 @@ class AccountScreen extends HookConsumerWidget { ], ).padding(horizontal: 16, vertical: 12), onTap: () { - context.pushNamed('creatorHub'); + context.goNamed('creatorHub'); }, ), ).height(140), diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index 891a00cc..ef45aa70 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -379,6 +379,21 @@ class CreatorHubScreen extends HookConsumerWidget { ); }, ), + ListTile( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + minTileHeight: 48, + title: const Text('webFeeds').tr(), + trailing: const Icon(Symbols.chevron_right), + leading: const Icon(Symbols.rss_feed), + onTap: () { + context.push('/creators/${currentPublisher.value!.name}/feeds'); + }, + ), + ]; + + final rightItems = [ ListTile( shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(8)), @@ -398,21 +413,6 @@ class CreatorHubScreen extends HookConsumerWidget { ); }, ), - ]; - - final rightItems = [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - minTileHeight: 48, - title: const Text('webFeeds').tr(), - trailing: const Icon(Symbols.chevron_right), - leading: const Icon(Symbols.rss_feed), - onTap: () { - context.push('/creators/${currentPublisher.value!.name}/feeds'); - }, - ), ExpansionTile( shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(8)), diff --git a/lib/screens/creators/poll/poll_list.dart b/lib/screens/creators/poll/poll_list.dart index 2fec8080..491584bf 100644 --- a/lib/screens/creators/poll/poll_list.dart +++ b/lib/screens/creators/poll/poll_list.dart @@ -106,10 +106,13 @@ class CreatorPollListScreen extends HookConsumerWidget { return endItemView; } final pollWithStats = data.items[index]; - return _CreatorPollItem( - pollWithStats: pollWithStats, - pubName: pubName, - ); + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: 640), + child: _CreatorPollItem( + pollWithStats: pollWithStats, + pubName: pubName, + ), + ).center(); }, ), ), diff --git a/lib/screens/creators/posts/post_manage_list.dart b/lib/screens/creators/posts/post_manage_list.dart index a0524d5c..825d579d 100644 --- a/lib/screens/creators/posts/post_manage_list.dart +++ b/lib/screens/creators/posts/post_manage_list.dart @@ -1,12 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/widgets/app_scaffold.dart'; -import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/post/post_list.dart'; -import 'package:material_symbols_icons/symbols.dart'; class CreatorPostListScreen extends HookConsumerWidget { final String pubName; @@ -16,63 +13,20 @@ class CreatorPostListScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final refreshKey = useState(0); - void showCreatePostSheet() { - showModalBottomSheet( - context: context, - builder: - (context) => SheetScaffold( - titleText: 'create'.tr(), - child: Column( - children: [ - ListTile( - leading: const Icon(Symbols.edit), - title: Text('Post'), - subtitle: Text('Create a regular post'), - onTap: () async { - Navigator.pop(context); - final result = await context.pushNamed( - 'postCompose', - queryParameters: {'type': '0'}, - ); - if (result == true) { - refreshKey.value++; - } - }, - ), - ListTile( - leading: const Icon(Symbols.article), - title: Text('Article'), - subtitle: Text('Create a detailed article'), - onTap: () async { - Navigator.pop(context); - final result = await context.pushNamed( - 'postCompose', - queryParameters: {'type': '1'}, - ); - if (result == true) { - refreshKey.value++; - } - }, - ), - ], - ), - ), - ); - } - return AppScaffold( isNoBackground: false, appBar: AppBar(title: Text('posts').tr()), body: CustomScrollView( key: ValueKey(refreshKey.value), slivers: [ - SliverPostList(pubName: pubName, itemType: PostItemType.creator), + SliverPostList( + pubName: pubName, + itemType: PostItemType.creator, + maxWidth: 640, + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: showCreatePostSheet, - child: const Icon(Symbols.add), - ), ); } } diff --git a/lib/screens/creators/webfeed/webfeed_edit.dart b/lib/screens/creators/webfeed/webfeed_edit.dart index 91b65d62..4f38de5e 100644 --- a/lib/screens/creators/webfeed/webfeed_edit.dart +++ b/lib/screens/creators/webfeed/webfeed_edit.dart @@ -6,25 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/webfeed.dart'; import 'package:island/pods/webfeed.dart'; import 'package:island/widgets/alert.dart'; -import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; -class WebFeedNewScreen extends StatelessWidget { - final String pubName; - const WebFeedNewScreen({super.key, required this.pubName}); - - @override - Widget build(BuildContext context) { - return WebFeedEditScreen(pubName: pubName, feedId: null); - } -} - -class WebFeedEditScreen extends HookConsumerWidget { +class WebfeedForm extends HookConsumerWidget { final String pubName; final String? feedId; - const WebFeedEditScreen({super.key, required this.pubName, this.feedId}); + const WebfeedForm({super.key, required this.pubName, this.feedId}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -113,16 +103,14 @@ class WebFeedEditScreen extends HookConsumerWidget { ); return feedAsync.when( - loading: - () => const AppScaffold( - isNoBackground: false, - body: Center(child: CircularProgressIndicator()), - ), + loading: () => const Center(child: CircularProgressIndicator()), error: - (error, stack) => AppScaffold( - isNoBackground: false, - appBar: AppBar(title: const Text('Error')), - body: Center(child: Text('Error: $error')), + (error, _) => ResponseErrorWidget( + error: error, + onRetry: + () => ref.invalidate( + webFeedNotifierProvider((pubName: pubName, feedId: feedId)), + ), ), data: (feed) { // Initialize form fields if they're empty and we have a feed @@ -133,160 +121,142 @@ class WebFeedEditScreen extends HookConsumerWidget { isScrapEnabled.value = feed.config.scrapPage; } - return _buildForm( - context, - formKey: formKey, - titleController: titleController, - urlController: urlController, - descriptionController: descriptionController, - isScrapEnabled: isScrapEnabled.value, - onScrapEnabledChanged: (value) => isScrapEnabled.value = value, - onSave: saveFeed, - onDelete: deleteFeed, - isLoading: isLoading.value, - ref: ref, - hasFeedId: feedId != null, + final scrapNow = useCallback(() async { + isLoading.value = true; + try { + await ref + .read( + webFeedNotifierProvider(( + pubName: pubName, + feedId: feedId!, + )).notifier, + ) + .scrapFeed(); + + if (context.mounted) { + showSnackBar('Feed scraping successfully.'); + } + } catch (e) { + showErrorAlert(e); + } finally { + isLoading.value = false; + } + }, [pubName, feedId, ref, context, isLoading]); + + final formFields = Column( + children: [ + TextFormField( + controller: titleController, + decoration: const InputDecoration( + labelText: 'Title', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: urlController, + decoration: const InputDecoration( + labelText: 'URL', + hintText: 'https://example.com/feed', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + keyboardType: TextInputType.url, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a URL'; + } + final uri = Uri.tryParse(value); + if (uri == null || !uri.hasAbsolutePath) { + return 'Please enter a valid URL'; + } + return null; + }, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: descriptionController, + decoration: const InputDecoration( + labelText: 'Description', + alignLabelWithHint: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + maxLines: 3, + ), + const SizedBox(height: 24), + Card( + margin: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + title: const Text('Scrape web page for content'), + subtitle: const Text( + 'When enabled, the system will attempt to extract full content from the web page', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + value: isScrapEnabled.value, + onChanged: (value) => isScrapEnabled.value = value, + ), + ], + ), + ), + const SizedBox(height: 20), + if (feedId != null) ...[ + TextButton.icon( + onPressed: isLoading.value ? null : scrapNow, + icon: const Icon(Symbols.refresh), + label: const Text('Scrape Now'), + ).alignment(Alignment.centerRight), + const SizedBox(height: 16), + ], + ], + ).padding(all: 20); + + final formWidget = Form( + key: formKey, + child: SingleChildScrollView(child: formFields), ); + + final buttonsRow = Row( + children: [ + if (feedId != null) + TextButton.icon( + onPressed: isLoading.value ? null : deleteFeed, + icon: const Icon(Symbols.delete_forever), + label: const Text('Delete Web Feed'), + style: TextButton.styleFrom(foregroundColor: Colors.red), + ), + const Spacer(), + TextButton.icon( + onPressed: isLoading.value ? null : saveFeed, + icon: const Icon(Symbols.save), + label: Text('saveChanges').tr(), + ), + ], + ).padding(horizontal: 20, vertical: 12); + + return Column(children: [Expanded(child: formWidget), buttonsRow]); }, ); } - - Widget _buildForm( - BuildContext context, { - required WidgetRef ref, - required GlobalKey formKey, - required TextEditingController titleController, - required TextEditingController urlController, - required TextEditingController descriptionController, - required bool isScrapEnabled, - required ValueChanged onScrapEnabledChanged, - required VoidCallback onSave, - required VoidCallback onDelete, - required bool isLoading, - required bool hasFeedId, - }) { - final scrapNow = useCallback(() async { - showLoadingModal(context); - try { - await ref - .read( - webFeedNotifierProvider(( - pubName: pubName, - feedId: feedId!, - )).notifier, - ) - .scrapFeed(); - - if (context.mounted) { - showSnackBar('Feed scraping successfully.'); - } - } catch (e) { - showErrorAlert(e); - } finally { - if (context.mounted) hideLoadingModal(context); - } - }, [pubName, feedId, ref, context]); - - return AppScaffold( - isNoBackground: false, - appBar: AppBar( - title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'), - actions: [ - if (hasFeedId) - IconButton( - icon: const Icon(Symbols.delete_forever), - onPressed: isLoading ? null : onDelete, - ), - const SizedBox(width: 8), - ], - ), - body: Form( - key: formKey, - child: SingleChildScrollView( - child: Column( - children: [ - TextFormField( - controller: titleController, - decoration: const InputDecoration(labelText: 'Title'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a title'; - } - return null; - }, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: urlController, - decoration: const InputDecoration( - labelText: 'URL', - hintText: 'https://example.com/feed', - ), - keyboardType: TextInputType.url, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a URL'; - } - final uri = Uri.tryParse(value); - if (uri == null || !uri.hasAbsolutePath) { - return 'Please enter a valid URL'; - } - return null; - }, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: descriptionController, - decoration: const InputDecoration( - labelText: 'Description', - alignLabelWithHint: true, - ), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - maxLines: 3, - ), - const SizedBox(height: 24), - Card( - margin: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SwitchListTile( - title: const Text('Scrape web page for content'), - subtitle: const Text( - 'When enabled, the system will attempt to extract full content from the web page', - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - value: isScrapEnabled, - onChanged: onScrapEnabledChanged, - ), - ], - ), - ), - const SizedBox(height: 20), - if (hasFeedId) ...[ - FilledButton.tonalIcon( - onPressed: isLoading ? null : scrapNow, - icon: const Icon(Symbols.refresh), - label: const Text('Scrape Now'), - ).alignment(Alignment.centerRight), - const SizedBox(height: 16), - ], - FilledButton.icon( - onPressed: isLoading ? null : onSave, - icon: const Icon(Symbols.save), - label: Text('saveChanges').tr(), - ).alignment(Alignment.centerRight), - ], - ).padding(all: 20), - ), - ), - ); - } } diff --git a/lib/screens/creators/webfeed/webfeed_list.dart b/lib/screens/creators/webfeed/webfeed_list.dart index b5ee67c7..fbc66f2d 100644 --- a/lib/screens/creators/webfeed/webfeed_list.dart +++ b/lib/screens/creators/webfeed/webfeed_list.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:island/pods/webfeed.dart'; +import 'package:island/screens/creators/webfeed/webfeed_edit.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/empty_state.dart'; import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; class WebFeedListScreen extends ConsumerWidget { final String pubName; @@ -22,9 +24,14 @@ class WebFeedListScreen extends ConsumerWidget { floatingActionButton: FloatingActionButton( child: const Icon(Symbols.add), onPressed: () { - context.pushNamed( - 'creatorFeedNew', - pathParameters: {'name': pubName}, + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => SheetScaffold( + titleText: 'New Web Feed', + child: WebfeedForm(pubName: pubName, feedId: null), + ), ); }, ), @@ -44,36 +51,47 @@ class WebFeedListScreen extends ConsumerWidget { itemCount: feeds.length, itemBuilder: (context, index) { final feed = feeds[index]; - return Card( - margin: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: 640), + child: Card( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + child: ListTile( + leading: const Icon(Symbols.rss_feed, size: 32), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + title: Text( + feed.title, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + feed.url, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => SheetScaffold( + titleText: 'Edit Web Feed', + child: WebfeedForm( + pubName: pubName, + feedId: feed.id, + ), + ), + ); + }, + ), ), - child: ListTile( - leading: const Icon(Symbols.rss_feed, size: 32), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - title: Text( - feed.title, - style: Theme.of(context).textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - feed.url, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - context.pushNamed( - 'creatorFeedEdit', - pathParameters: {'name': pubName, 'feedId': feed.id}, - ); - }, - ), - ); + ).center(); }, ), ); diff --git a/lib/screens/poll/poll_editor.dart b/lib/screens/poll/poll_editor.dart index b67149b3..b6b2e4e1 100644 --- a/lib/screens/poll/poll_editor.dart +++ b/lib/screens/poll/poll_editor.dart @@ -434,177 +434,198 @@ class PollEditorScreen extends ConsumerWidget { body: Column( children: [ Expanded( - child: Form( - key: ValueKey(model.id), - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - TextFormField( - initialValue: model.title ?? '', - decoration: InputDecoration( - labelText: 'title'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ), - ), - textInputAction: TextInputAction.next, - maxLength: 256, - onChanged: notifier.setTitle, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - validator: (v) { - if (v == null || v.trim().isEmpty) { - return 'pollTitleRequired'.tr(); - } - return null; - }, - ), - const Gap(12), - TextFormField( - initialValue: model.description ?? '', - decoration: InputDecoration( - labelText: 'description'.tr(), - alignLabelWithHint: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ), - ), - maxLines: 3, - maxLength: 4096, - onChanged: notifier.setDescription, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const Gap(12), - _EndDatePicker( - value: model.endedAt, - onChanged: notifier.setEndedAt, - ), - const Gap(24), - Row( - children: [ - Text( - 'questions'.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const Spacer(), - MenuAnchor( - builder: (context, controller, child) { - return FilledButton.icon( - onPressed: () { - controller.isOpen - ? controller.close() - : controller.open(); - }, - icon: const Icon(Icons.add), - label: Text('pollAddQuestion'.tr()), - ); - }, - menuChildren: - SnPollQuestionType.values - .map( - (t) => MenuItemButton( - leadingIcon: Icon(_iconForType(t)), - onPressed: () => notifier.addQuestion(t), - child: Text(_labelForType(t)), - ), - ) - .toList(), - ), - ], - ), - const Gap(8), - if (model.questions.isEmpty) - _EmptyState( - title: 'pollNoQuestionsYet'.tr(), - subtitle: 'pollNoQuestionsHint'.tr(), - ) - else - ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: model.questions.length, - onReorder: (oldIndex, newIndex) { - // Convert to stepwise moves using provided functions - if (newIndex > oldIndex) newIndex -= 1; - final steps = newIndex - oldIndex; - if (steps == 0) return; - if (steps > 0) { - for (int i = 0; i < steps; i++) { - notifier.moveQuestionDown(oldIndex + i); - } - } else { - for (int i = 0; i > steps; i--) { - notifier.moveQuestionUp(oldIndex + i); - } - } - }, - buildDefaultDragHandles: false, - itemBuilder: (context, index) { - final q = model.questions[index]; - return Card( - key: ValueKey('q_$index'), - margin: const EdgeInsets.symmetric(vertical: 8), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - _QuestionHeader( - index: index, - question: q, - onMoveUp: - index > 0 - ? () => notifier.moveQuestionUp(index) - : null, - onMoveDown: - index < model.questions.length - 1 - ? () => notifier.moveQuestionDown(index) - : null, - onDelete: () => notifier.removeQuestion(index), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 640), + child: + Form( + key: ValueKey(model.id), + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + TextFormField( + initialValue: model.title ?? '', + decoration: InputDecoration( + labelText: 'postTitle'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(16), ), - const Divider(height: 1), - Padding( - padding: const EdgeInsets.all(16), - child: _QuestionEditor( - index: index, - question: q, - ), - ), - ], + ), ), - ); - }, + textInputAction: TextInputAction.next, + maxLength: 256, + onChanged: notifier.setTitle, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + validator: (v) { + if (v == null || v.trim().isEmpty) { + return 'pollTitleRequired'.tr(); + } + return null; + }, + ), + const Gap(12), + TextFormField( + initialValue: model.description ?? '', + decoration: InputDecoration( + labelText: 'description'.tr(), + alignLabelWithHint: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(16), + ), + ), + ), + maxLines: 3, + maxLength: 4096, + onChanged: notifier.setDescription, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(12), + _EndDatePicker( + value: model.endedAt, + onChanged: notifier.setEndedAt, + ), + const Gap(24), + Row( + children: [ + Text( + 'questions'.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), + const Spacer(), + MenuAnchor( + builder: (context, controller, child) { + return FilledButton.icon( + onPressed: () { + controller.isOpen + ? controller.close() + : controller.open(); + }, + icon: const Icon(Icons.add), + label: Text('pollAddQuestion'.tr()), + ); + }, + menuChildren: + SnPollQuestionType.values + .map( + (t) => MenuItemButton( + leadingIcon: Icon(_iconForType(t)), + onPressed: + () => notifier.addQuestion(t), + child: Text(_labelForType(t)), + ), + ) + .toList(), + ), + ], + ), + const Gap(8), + if (model.questions.isEmpty) + _EmptyState( + title: 'pollNoQuestionsYet'.tr(), + subtitle: 'pollNoQuestionsHint'.tr(), + ) + else + ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: model.questions.length, + onReorder: (oldIndex, newIndex) { + // Convert to stepwise moves using provided functions + if (newIndex > oldIndex) newIndex -= 1; + final steps = newIndex - oldIndex; + if (steps == 0) return; + if (steps > 0) { + for (int i = 0; i < steps; i++) { + notifier.moveQuestionDown(oldIndex + i); + } + } else { + for (int i = 0; i > steps; i--) { + notifier.moveQuestionUp(oldIndex + i); + } + } + }, + buildDefaultDragHandles: false, + itemBuilder: (context, index) { + final q = model.questions[index]; + return Card( + key: ValueKey('q_$index'), + margin: const EdgeInsets.symmetric(vertical: 8), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + _QuestionHeader( + index: index, + question: q, + onMoveUp: + index > 0 + ? () => + notifier.moveQuestionUp(index) + : null, + onMoveDown: + index < model.questions.length - 1 + ? () => notifier.moveQuestionDown( + index, + ) + : null, + onDelete: + () => notifier.removeQuestion(index), + ), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(16), + child: _QuestionEditor( + index: index, + question: q, + ), + ), + ], + ), + ); + }, + ), + const Gap(96), + ], ), - const Gap(96), - ], - ), + ).center(), ), ), Material( elevation: 2, color: Theme.of(context).colorScheme.surfaceContainer, - child: Row( - children: [ - OutlinedButton.icon( - onPressed: () { - Navigator.of(context).maybePop(); - }, - icon: const Icon(Icons.close), - label: Text('cancel'.tr()), - ), - const Spacer(), - FilledButton.icon( - onPressed: () { - _submitPoll(context, ref); - }, - icon: const Icon(Icons.cloud_upload_outlined), - label: Text(model.id == null ? 'create'.tr() : 'update'.tr()), - ), - ], - ).padding( - horizontal: 24, - top: 16, - bottom: MediaQuery.of(context).padding.bottom + 16, - ), + child: + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 640), + child: Row( + children: [ + OutlinedButton.icon( + onPressed: () { + Navigator.of(context).maybePop(); + }, + icon: const Icon(Icons.close), + label: Text('cancel'.tr()), + ), + const Spacer(), + FilledButton.icon( + onPressed: () { + _submitPoll(context, ref); + }, + icon: const Icon(Icons.cloud_upload_outlined), + label: Text( + model.id == null ? 'create'.tr() : 'update'.tr(), + ), + ), + ], + ).padding( + horizontal: 24, + top: 16, + bottom: MediaQuery.of(context).padding.bottom + 16, + ), + ).center(), ), ], ), diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index d8fd79ae..0252318c 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -149,18 +149,13 @@ class SliverPostList extends HookConsumerWidget { Widget _buildPostItem(SnPost post) { switch (itemType) { case PostItemType.creator: - return Column( - children: [ - PostItemCreator( - item: post, - backgroundColor: backgroundColor, - padding: padding, - isOpenable: isOpenable, - onRefresh: onRefresh, - onUpdate: onUpdate, - ), - const Divider(), - ], + return PostItemCreator( + item: post, + backgroundColor: backgroundColor, + padding: padding, + isOpenable: isOpenable, + onRefresh: onRefresh, + onUpdate: onUpdate, ); case PostItemType.regular: return Card(