import 'dart:async'; import 'package:app_links/app_links.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/intent/links.intent.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/views/chats/chat_list.view.dart'; import 'package:twonly/src/visual/views/memories/memories.view.dart'; class HomeView extends StatefulWidget { const HomeView({ required this.initialPage, super.key, }); final int initialPage; @override State createState() => HomeViewState(); } class HomeViewState extends State with WidgetsBindingObserver { int _activePageIdx = 1; double _offsetRatio = 0; double _offsetFromOne = 0; bool _isBottomNavVisible = true; Timer? _disableCameraTimer; final MainCameraController _mainCameraController = MainCameraController(); late final PageController _homeViewPageController; StreamSubscription>? _intentStreamSub; StreamSubscription? _deepLinkSub; StreamSubscription? _onMessageOpenedAppSub; StreamSubscription? _homeViewPageIndexSub; StreamSubscription? _selectNotificationSub; static final streamHomeViewPageIndex = StreamController.broadcast(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); var initialPage = widget.initialPage; if (initialPage == 1 && !userService.currentUser.startWithCameraOpen) { initialPage = 0; } _activePageIdx = initialPage; _offsetFromOne = 1.0 - initialPage; _offsetRatio = _offsetFromOne.abs(); _homeViewPageController = PageController(initialPage: initialPage); _mainCameraController.setState = () { if (mounted) setState(() {}); }; _homeViewPageIndexSub = streamHomeViewPageIndex.stream.listen((index) { if (_homeViewPageController.hasClients) { _homeViewPageController.jumpToPage(index); } setState(() { _activePageIdx = index; _offsetFromOne = 1.0 - index; _offsetRatio = _offsetFromOne.abs(); }); if (index != 1) { unawaited(_mainCameraController.closeCamera()); } }); _selectNotificationSub = selectNotificationStream.stream.listen(( response, ) async { if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) { routerProvider.go(response.payload!); } streamHomeViewPageIndex.add(0); }); _onMessageOpenedAppSub = FirebaseMessaging.onMessageOpenedApp.listen(( message, ) { Log.info('Opened app from iOS/Remote push notification tap.'); streamHomeViewPageIndex.add(0); }); if (initialPage == 1) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_isViewActive()) { unawaited(_mainCameraController.selectCamera(0, true)); } }); } unawaited(_initAsync()); void handleShareLink(Uri uri) { routerProvider.go(Routes.home); streamHomeViewPageIndex.add(1); _mainCameraController.setSharedLinkForPreview(uri); } // Subscribe to all events (initial link and further) _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { if (!mounted) return; Log.info('Got link via app links: ${uri.scheme}'); if (!await handleIntentUrl(context, uri)) { if (uri.scheme.startsWith('http')) { handleShareLink(uri); } } }); _intentStreamSub = initIntentStreams( context, handleShareLink, ); WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) { streamHomeViewPageIndex.add(0); } }); } Future _initAsync() async { final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); RemoteMessage? initialRemoteMessage; try { initialRemoteMessage = await FirebaseMessaging.instance .getInitialMessage(); } catch (e) { Log.error('Could not get initial Firebase message: $e'); } if (widget.initialPage == 0 || initialRemoteMessage != null || (notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) { if (initialRemoteMessage != null) { Log.info('App launched from iOS/Remote push notification tap.'); streamHomeViewPageIndex.add(0); } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { final payload = notificationAppLaunchDetails?.notificationResponse?.payload; if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) { routerProvider.go(payload); } streamHomeViewPageIndex.add(0); } } final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile(); if (draftMedia != null) { if (!mounted) return; await Navigator.push( context, MaterialPageRoute( builder: (context) => ShareImageEditorView( mediaFileService: MediaFileService(draftMedia), sharedFromGallery: true, ), ), ); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _onMessageOpenedAppSub?.cancel(); _homeViewPageIndexSub?.cancel(); _selectNotificationSub?.cancel(); _disableCameraTimer?.cancel(); _mainCameraController.setState = null; _mainCameraController.closeCamera(); _intentStreamSub?.cancel(); _deepLinkSub?.cancel(); super.dispose(); } bool _isViewActive() { if (!mounted) return false; return ModalRoute.of(context)?.isCurrent ?? false; } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { if (_offsetRatio < 1 && !_mainCameraController.isSharePreviewIsShown && _isViewActive() && _mainCameraController.cameraController == null && !_mainCameraController.initCameraStarted) { unawaited( _mainCameraController.selectCamera( _mainCameraController.selectedCameraDetails.cameraId, false, ), ); } } else if (state == AppLifecycleState.paused) { unawaited(_mainCameraController.closeCamera()); } } bool _onPageView(ScrollNotification notification) { _disableCameraTimer?.cancel(); if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) { final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent; if (!canScroll) { if (!_isBottomNavVisible) { setState(() { _isBottomNavVisible = true; }); } } else { if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) { setState(() { _isBottomNavVisible = true; }); } else if (notification is ScrollUpdateNotification) { final delta = notification.scrollDelta ?? 0; if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) { setState(() { _isBottomNavVisible = false; }); } else if (delta < -5 && !_isBottomNavVisible) { setState(() { _isBottomNavVisible = true; }); } } } } if (notification.depth == 0) { setState(() { _offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0); _offsetRatio = _offsetFromOne.abs(); final pageIndex = _homeViewPageController.page?.round(); if (pageIndex != null && pageIndex != _activePageIdx) { _activePageIdx = pageIndex; } }); } if (_mainCameraController.cameraController == null && !_mainCameraController.initCameraStarted && _offsetRatio < 1 && _isViewActive()) { unawaited( _mainCameraController.selectCamera( _mainCameraController.selectedCameraDetails.cameraId, false, ), ); } if (_offsetRatio == 1) { _disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { await _mainCameraController.closeCamera(); _mainCameraController.sharedLinkForPreview = null; _disableCameraTimer = null; }); } return false; } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ MainCameraPreview(mainCameraController: _mainCameraController), Positioned.fill( child: Opacity( opacity: _offsetRatio, child: Container( color: context.color.surface, ), ), ), NotificationListener( onNotification: _onPageView, child: Positioned.fill( child: CustomScrollView( scrollDirection: Axis.horizontal, physics: const PageScrollPhysics(), controller: _homeViewPageController, scrollCacheExtent: const ScrollCacheExtent.viewport(1), slivers: [ SliverFillViewport( delegate: SliverChildListDelegate([ const ChatListView(), Container(), const MemoriesView(), ]), ), ], ), ), ), Positioned.fill( child: _offsetRatio == 0 ? GestureDetector( behavior: HitTestBehavior.translucent, onDoubleTap: _mainCameraController.onDoubleTap, onTapDown: _mainCameraController.onTapDown, ) : const SizedBox.shrink(), ), Positioned( key: const ValueKey('camera_controls'), left: 0, top: 0, right: 0, bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0, child: Opacity( opacity: 1 - (_offsetRatio * 4) % 1, child: CameraPreviewControllerView( mainController: _mainCameraController, isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1, ), ), ), ], ), bottomNavigationBar: AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: (_activePageIdx != 2 || _isBottomNavVisible) ? BottomNavigationBar( showSelectedLabels: false, showUnselectedLabels: false, unselectedIconTheme: IconThemeData( color: Theme.of( context, ).colorScheme.inverseSurface.withAlpha(150), ), selectedIconTheme: IconThemeData( color: Theme.of(context).colorScheme.inverseSurface, ), items: const [ BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.solidComments), label: '', ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.camera), label: '', ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.photoFilm), label: '', ), ], onTap: (index) async { _activePageIdx = index; await _homeViewPageController.animateToPage( index, duration: const Duration(milliseconds: 100), curve: Curves.bounceIn, ); if (mounted) setState(() {}); }, currentIndex: _activePageIdx, ) : const SizedBox.shrink(), ), ); } }