diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 513c0a75..19410c42 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -19,7 +19,6 @@ import 'package:island/widgets/check_in.dart'; import 'package:island/widgets/post/post_featured.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/compose_card.dart'; -import 'package:island/widgets/post/compose_dialog.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -91,10 +90,6 @@ class ExploreScreen extends HookConsumerWidget { return () => tabController.removeListener(listener); }, [tabController]); - final activitiesNotifier = ref.watch( - activityListNotifierProvider(currentFilter.value).notifier, - ); - final now = DateTime.now(); final query = useState( @@ -213,27 +208,6 @@ class ExploreScreen extends HookConsumerWidget { return AppScaffold( isNoBackground: false, - floatingActionButton: - isWide - ? null - : InkWell( - onLongPress: () async { - final result = await PostComposeDialog.show(context); - if (result != null) { - activitiesNotifier.forceRefresh(); - } - }, - child: FloatingActionButton( - heroTag: Key("explore-page-fab"), - onPressed: () async { - final result = await PostComposeDialog.show(context); - if (result != null) { - activitiesNotifier.forceRefresh(); - } - }, - child: const Icon(Symbols.edit), - ), - ), body: isWide ? _buildWideBody( @@ -334,11 +308,7 @@ class ExploreScreen extends HookConsumerWidget { margin: EdgeInsets.zero, ), PostFeaturedList(), - PostComposeCard( - onSubmit: () { - activitiesNotifier.forceRefresh(); - }, - ), + const PostComposeCard(), ], ), ), diff --git a/lib/screens/tabs.dart b/lib/screens/tabs.dart index 129000a8..bab5c7ef 100644 --- a/lib/screens/tabs.dart +++ b/lib/screens/tabs.dart @@ -3,11 +3,13 @@ import 'dart:ui'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:island/screens/notification.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/navigation/conditional_bottom_nav.dart'; +import 'package:island/widgets/post/compose_dialog.dart'; import 'package:material_symbols_icons/symbols.dart'; final currentRouteProvider = StateProvider((ref) => null); @@ -94,6 +96,12 @@ class TabsScreen extends HookConsumerWidget { final currentIndex = getCurrentIndex(); + final routes = kTabRoutes.sublist( + 0, + isWideScreen(context) ? null : kWideScreenRouteStart, + ); + final shouldShowFab = routes.contains(currentLocation) && !wideScreen; + if (isWideScreen(context)) { return Container( color: Theme.of(context).colorScheme.surfaceContainer, @@ -137,29 +145,109 @@ class TabsScreen extends HookConsumerWidget { ), child: child ?? const SizedBox.shrink(), ), + floatingActionButton: + shouldShowFab + ? FloatingActionButton( + child: const Icon(Symbols.menu), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Gap(24), + ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Symbols.post_add_rounded), + title: Text('postCompose'.tr()), + onTap: () async { + Navigator.of(context).pop(); + await PostComposeDialog.show(context); + }, + ), + Consumer( + builder: (context, ref, _) { + final notificationCount = ref.watch( + notificationUnreadCountNotifierProvider, + ); + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Symbols.notifications), + trailing: Badge( + label: Text(notificationCount.toString()), + isLabelVisible: notificationCount.value! > 0, + ), + title: Text('notifications'.tr()), + onTap: () async { + Navigator.of(context).pop(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: + (context) => const NotificationSheet(), + ); + }, + ); + }, + ), + Gap(MediaQuery.of(context).padding.bottom + 16), + ], + ); + }, + ); + }, + ) + : null, + floatingActionButtonLocation: + shouldShowFab ? TabbedFabLocation(context) : null, bottomNavigationBar: ConditionalBottomNav( child: ClipRRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 1, sigmaY: 1), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface.withOpacity(0.8), - ), - child: MediaQuery.removePadding( - context: context, - removeTop: true, - child: NavigationBar( - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - overlayColor: const WidgetStatePropertyAll( - Colors.transparent, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: BottomAppBar( + height: 56, + padding: EdgeInsets.symmetric(horizontal: 24), + shape: AutomaticNotchedShape( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), ), - surfaceTintColor: Colors.transparent, - height: 56, - labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, - selectedIndex: currentIndex, - onDestinationSelected: onDestinationSelected, - destinations: destinations, + ), + color: Theme.of(context).colorScheme.surface.withOpacity(0.8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: () { + final navItems = + destinations.asMap().entries.map((entry) { + int index = entry.key; + NavigationDestination dest = entry.value; + return IconButton( + icon: dest.icon, + onPressed: () => onDestinationSelected(index), + color: + index == currentIndex + ? Theme.of(context).colorScheme.primary + : null, + ); + }).toList(); + // Add mock item in the center to leave space for FAB + int centerIndex = navItems.length ~/ 2; + navItems.insert(centerIndex, const SizedBox(width: 72)); + return navItems; + }(), ), ), ), @@ -180,14 +268,13 @@ class TabbedFabLocation extends FloatingActionButtonLocation { final mediaQuery = MediaQuery.of(context); final safeAreaPadding = mediaQuery.padding; - // Calculate position with proper safe area considerations + // Center horizontally final double fabX = - scaffoldGeometry.scaffoldSize.width - - scaffoldGeometry.floatingActionButtonSize.width - - 16 - - safeAreaPadding.right; + (scaffoldGeometry.scaffoldSize.width - + scaffoldGeometry.floatingActionButtonSize.width) / + 2; - // Use safe area bottom padding + navigation bar height (typically 80px) + // Position closer to bottom with reduced padding final double fabY = scaffoldGeometry.scaffoldSize.height - scaffoldGeometry.floatingActionButtonSize.height - diff --git a/lib/services/event_bus.dart b/lib/services/event_bus.dart new file mode 100644 index 00000000..2c4bcecc --- /dev/null +++ b/lib/services/event_bus.dart @@ -0,0 +1,13 @@ +import 'package:event_bus/event_bus.dart'; + +/// Global event bus instance for the application +final eventBus = EventBus(); + +/// Event fired when a post is successfully created +class PostCreatedEvent { + final String? postId; + final String? title; + final String? content; + + const PostCreatedEvent({this.postId, this.title, this.content}); +} diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 5b658c30..79e32d07 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -337,7 +337,6 @@ class AppScaffold extends HookConsumerWidget { endDrawer: endDrawer, floatingActionButton: floatingActionButton, floatingActionButtonAnimator: floatingActionButtonAnimator, - floatingActionButtonLocation: TabbedFabLocation(context), onDrawerChanged: onDrawerChanged, onEndDrawerChanged: onEndDrawerChanged, ), diff --git a/lib/widgets/post/compose_dialog.dart b/lib/widgets/post/compose_dialog.dart index 9a6aaeb5..3d38e363 100644 --- a/lib/widgets/post/compose_dialog.dart +++ b/lib/widgets/post/compose_dialog.dart @@ -6,6 +6,7 @@ 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/services/responsive.dart'; import 'package:island/widgets/post/compose_card.dart'; @@ -74,7 +75,11 @@ class PostComposeDialog extends HookConsumerWidget { originalPost: originalPost, initialState: restoredInitialState.value ?? initialState, onCancel: () => Navigator.of(context).pop(), - onSubmit: () => Navigator.of(context).pop(true), + onSubmit: () { + // Fire event to notify listeners that a post was created + eventBus.fire(PostCreatedEvent()); + Navigator.of(context).pop(true); + }, isDialog: true, ), ), diff --git a/pubspec.lock b/pubspec.lock index 90bb781b..8905070b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -545,6 +545,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.7" + event_bus: + dependency: "direct main" + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" expandable: dependency: transitive description: @@ -782,6 +790,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flutter_expandable_fab: + dependency: "direct main" + description: + name: flutter_expandable_fab + sha256: "2a488600924fd2a041679ad889807ee5670414a7a518cf11d4854b9898b3504f" + url: "https://pub.dev" + source: hosted + version: "2.5.2" flutter_highlight: dependency: "direct main" description: @@ -1137,10 +1153,10 @@ packages: dependency: transitive description: name: font_awesome_flutter - sha256: ef8e9591f6de2bf671c3b6f506f5ff85f03d34403084fccced62d3628fb086b9 + sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 url: "https://pub.dev" source: hosted - version: "10.11.0" + version: "10.12.0" freezed: dependency: "direct dev" description: @@ -1201,10 +1217,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: e1d7ffb0db475e6e845eb58b44768f50b830e23960e3df6908924acd8f7f70ea + sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c url: "https://pub.dev" source: hosted - version: "16.2.5" + version: "16.3.0" google_fonts: dependency: "direct main" description: @@ -1361,10 +1377,10 @@ packages: dependency: "direct main" description: name: image_picker_platform_interface - sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.11.1" image_picker_windows: dependency: transitive description: @@ -1473,10 +1489,10 @@ packages: dependency: "direct main" description: name: livekit_client - sha256: c70dc6a16cd7e8c1420b7c7ab65f2bd1142db06fb7a873aaa1dc224cc69d33a6 + sha256: ddb4467d306be472898b2459c87768121aba030173b3664ef367f7f7f4c96897 url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" local_auth: dependency: "direct main" description: @@ -2535,18 +2551,18 @@ packages: dependency: transitive description: name: syncfusion_flutter_core - sha256: adcd41bc5c4de1e7aa831fe3f2ca2d22465de29f166a9de685133b70d21e4541 + sha256: d03c43f577cdbe020d1632bece00cbf8bec4a7d0ab123923b69141b5fec35420 url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_flutter_pdf: dependency: transitive description: name: syncfusion_flutter_pdf - sha256: "4e87a865053879ebbe79076bd75e9763b483455936597f9f0a424c4f87f8abc1" + sha256: cb16c8631ab390fdd547c0661f3c8ab7a417ce0f4d7f47a6b8a0811b9bd23b2d url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_flutter_pdfviewer: dependency: "direct main" description: @@ -2559,50 +2575,50 @@ packages: dependency: transitive description: name: syncfusion_flutter_signaturepad - sha256: "355a71cd37b9fe5e92658dd10d56fbacdcfea109a542663e0701ff71c3609e4c" + sha256: "73c73ad0779f772084493bed59124b069e30ae295f4d35ae81dc5a7513198d97" url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_pdfviewer_linux: dependency: transitive description: name: syncfusion_pdfviewer_linux - sha256: d7b1cbbc06d28a698034311a781dbdd97390035553ea62d44c7d95505e836d85 + sha256: a69242b0ced822e190a5cba8791cb203999da372f6c67f038d14dda799ecfb80 url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_pdfviewer_macos: dependency: transitive description: name: syncfusion_pdfviewer_macos - sha256: "22c6ce2a564b9580ad97f373774094267bb9bc6ea8512f125c325018b41eb09d" + sha256: "0253828d6c07e4a5ade5afe528045bd047fbccf907823ae57811b6bbf09a5b2f" url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_pdfviewer_platform_interface: dependency: transitive description: name: syncfusion_pdfviewer_platform_interface - sha256: "7976dc9c29e8f0cb4e71c1fc42db8ae9ba60fc73206d750c8a9b39efd9c46e31" + sha256: "00aef95383dd457e868ec00a0babc25a669f3ee3c30a49b230f561257349b965" url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_pdfviewer_web: dependency: transitive description: name: syncfusion_pdfviewer_web - sha256: "6c630e710b18854f2ca370a23966c870b1a25e026fd9a42191dce7a23d28cac3" + sha256: "87fbbec373cd80f231bb5c48dcb69808ba55acb9fb81a7423b959ec8a7cddf77" url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" syncfusion_pdfviewer_windows: dependency: transitive description: name: syncfusion_pdfviewer_windows - sha256: "8ef5e72cd43ed739b5689ab31c825a11e0ff85225c1e0e363ee13587fae2f7bb" + sha256: "3b9ec92595e75c65be0a9514f61c566c8fc1b1601ab97927b958276de395ca9f" url: "https://pub.dev" source: hosted - version: "31.2.2" + version: "31.2.3" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e92dabf6..83fd9d3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: cupertino_icons: ^1.0.8 flutter_hooks: ^0.21.3+1 hooks_riverpod: ^2.6.1 - go_router: ^16.2.5 + go_router: ^16.3.0 styled_widget: ^0.4.1 shared_preferences: ^2.5.3 flutter_riverpod: ^2.6.1 @@ -74,7 +74,7 @@ dependencies: image_picker: ^1.2.0 file_picker: ^10.3.3 riverpod_annotation: ^2.6.1 - image_picker_platform_interface: ^2.11.0 + image_picker_platform_interface: ^2.11.1 image_picker_android: ^0.8.13+5 super_context_menu: ^0.9.1 modal_bottom_sheet: ^3.0.0 @@ -102,7 +102,7 @@ dependencies: gal: ^2.3.2 dismissible_page: ^1.0.2 super_sliver_list: ^0.4.1 - livekit_client: ^2.5.2 + livekit_client: ^2.5.3 pasteboard: ^0.4.0 flutter_colorpicker: ^1.1.0 image: ^4.5.4 @@ -163,6 +163,8 @@ dependencies: swipe_to: ^1.0.6 fl_heatmap: ^0.4.5 dio_smart_retry: ^7.0.1 + flutter_expandable_fab: ^2.5.2 + event_bus: ^2.0.1 dev_dependencies: flutter_test: