From f5241a72fad684fe013da787fc8270dbaabc6cf5 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 24 Jul 2024 11:42:24 -0400 Subject: [PATCH] Add initial basic focus mode implementation --- lib/controls/focus_mode_status_headline.dart | 55 +++ lib/controls/standard_app_drawer.dart | 2 + lib/main.dart | 39 +- lib/routes.dart | 460 +++++++++---------- lib/screens/home.dart | 29 +- 5 files changed, 332 insertions(+), 253 deletions(-) create mode 100644 lib/controls/focus_mode_status_headline.dart diff --git a/lib/controls/focus_mode_status_headline.dart b/lib/controls/focus_mode_status_headline.dart new file mode 100644 index 0000000..dfcd266 --- /dev/null +++ b/lib/controls/focus_mode_status_headline.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../utils/dateutils.dart'; + +class AppBottomFocusModeStatus extends StatefulWidget { + final DateTime? disableTime; + + const AppBottomFocusModeStatus({super.key, required this.disableTime}); + + @override + State createState() => + _AppBottomFocusModeStatusState(); +} + +class _AppBottomFocusModeStatusState extends State { + Timer? updateTimer; + Duration? timeUntil; + + @override + void initState() { + super.initState(); + _updateTimeUntil(); + updateTimer = Timer.periodic(Duration(seconds: 1), (_) { + setState(() { + _updateTimeUntil(); + }); + }); + } + + @override + void dispose() { + print('Disposing'); + updateTimer?.cancel(); + super.dispose(); + } + + void _updateTimeUntil() { + timeUntil = widget.disableTime?.difference(DateTime.now()); + } + + @override + Widget build(BuildContext context) { + final title = timeUntil == null + ? 'Focus Mode' + : 'Focus Mode for ${timeUntil!.simpleLabel}'; + return Text( + title, + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + fontWeight: FontWeight.bold, + ), + ); + } +} diff --git a/lib/controls/standard_app_drawer.dart b/lib/controls/standard_app_drawer.dart index 8a54540..f4be0c5 100644 --- a/lib/controls/standard_app_drawer.dart +++ b/lib/controls/standard_app_drawer.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import '../globals.dart'; import '../routes.dart'; import '../services/auth_service.dart'; +import 'focus_mode_menu_item.dart'; import 'login_aware_cached_network_image.dart'; class StandardAppDrawer extends StatelessWidget { @@ -62,6 +63,7 @@ class StandardAppDrawer extends StatelessWidget { 'Manage Profiles', () => context.pushNamed(ScreenPaths.manageProfiles), ), + const FocusModeMenuItem(), const Divider(), buildMenuButton( context, diff --git a/lib/main.dart b/lib/main.dart index a222708..0f92aee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart' as fr; +import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:media_kit/media_kit.dart'; import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; @@ -8,6 +10,7 @@ import 'package:provider/provider.dart'; import 'app_theme.dart'; import 'di_initialization.dart'; import 'globals.dart'; +import 'riverpod_controllers/focus_mode.dart'; import 'routes.dart'; import 'services/auth_service.dart'; import 'services/blocks_manager.dart'; @@ -51,7 +54,7 @@ void main() async { // enabled: !kReleaseMode && enablePreview, // builder: (context) => const App(), // )); - runApp(const App()); + runApp(const fr.ProviderScope(child: App())); } Future setupPackageInfoAndUserAgent() async { @@ -60,13 +63,45 @@ Future setupPackageInfoAndUserAgent() async { userAgent = 'Relatica/$appVersion'; } -class App extends StatelessWidget { +class App extends fr.ConsumerStatefulWidget { const App({super.key}); // This widget is the root of your application. + @override + fr.ConsumerState createState() => _AppState(); +} + +class _AppState extends fr.ConsumerState { @override Widget build(BuildContext context) { final settingsService = getIt(); + final authService = getIt(); + + final appRouter = GoRouter( + initialLocation: ScreenPaths.timelines, + debugLogDiagnostics: true, + refreshListenable: authService, + redirect: (context, state) async { + final loggedIn = authService.loggedIn; + final focusMode = ref.read(focusModeProvider); + print('Focus mode? $focusMode'); + + if (!loggedIn && authService.initializing) { + return ScreenPaths.splash; + } + + if (!loggedIn && !allowedLoggedOut.contains(state.uri.toString())) { + return ScreenPaths.signin; + } + + if (loggedIn && allowedLoggedOut.contains(state.uri.toString())) { + return ScreenPaths.timelines; + } + + return null; + }, + routes: routes, + ); return AnimatedBuilder( builder: (context, child) { Logger.root.level = settingsService.logLevel; diff --git a/lib/routes.dart b/lib/routes.dart index afb0ac7..8457991 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,6 +1,5 @@ import 'package:go_router/go_router.dart'; -import 'globals.dart'; import 'models/interaction_type_enum.dart'; import 'screens/blocks_screen.dart'; import 'screens/circle_add_users_screen.dart'; @@ -29,7 +28,6 @@ import 'screens/sign_in.dart'; import 'screens/splash.dart'; import 'screens/user_posts_screen.dart'; import 'screens/user_profile_screen.dart'; -import 'services/auth_service.dart'; class ScreenPaths { static String blocks = '/blocks'; @@ -56,271 +54,249 @@ class ScreenPaths { } bool needAuthChangeInitialized = true; -final _authService = getIt(); final allowedLoggedOut = [ ScreenPaths.splash, ScreenPaths.signin, ScreenPaths.signup ]; -final appRouter = GoRouter( - initialLocation: ScreenPaths.timelines, - debugLogDiagnostics: true, - refreshListenable: _authService, - redirect: (context, state) async { - final loggedIn = _authService.loggedIn; - - if (!loggedIn && _authService.initializing) { - return ScreenPaths.splash; - } - - if (!loggedIn && !allowedLoggedOut.contains(state.uri.toString())) { - return ScreenPaths.signin; - } - - if (loggedIn && allowedLoggedOut.contains(state.uri.toString())) { - return ScreenPaths.timelines; - } - - return null; - }, +final routes = [ + GoRoute( + path: ScreenPaths.blocks, + name: ScreenPaths.blocks, + builder: (context, state) => const BlocksScreen(), + ), + GoRoute( + path: ScreenPaths.filters, + name: ScreenPaths.filters, + builder: (context, state) => const FiltersScreen(), routes: [ GoRoute( - path: ScreenPaths.blocks, - name: ScreenPaths.blocks, - builder: (context, state) => const BlocksScreen(), + path: 'new', + pageBuilder: (context, state) => const NoTransitionPage( + child: FilterEditorScreen(id: ''), + ), ), GoRoute( - path: ScreenPaths.filters, - name: ScreenPaths.filters, - builder: (context, state) => const FiltersScreen(), - routes: [ - GoRoute( - path: 'new', - pageBuilder: (context, state) => const NoTransitionPage( - child: FilterEditorScreen(id: ''), - ), - ), - GoRoute( - path: 'edit/:id', - pageBuilder: (context, state) => NoTransitionPage( - child: FilterEditorScreen(id: state.pathParameters['id']!)), - ) - ], - ), - GoRoute( - path: ScreenPaths.signin, - name: ScreenPaths.signin, - builder: (context, state) => const SignInScreen(), - ), - GoRoute( - path: ScreenPaths.manageProfiles, - name: ScreenPaths.manageProfiles, - builder: (context, state) => const SignInScreen(), - ), - GoRoute( - path: ScreenPaths.contacts, - name: ScreenPaths.contacts, + path: 'edit/:id', pageBuilder: (context, state) => NoTransitionPage( - name: ScreenPaths.contacts, - child: const ContactsScreen(), - ), - ), + child: FilterEditorScreen(id: state.pathParameters['id']!)), + ) + ], + ), + GoRoute( + path: ScreenPaths.signin, + name: ScreenPaths.signin, + builder: (context, state) => const SignInScreen(), + ), + GoRoute( + path: ScreenPaths.manageProfiles, + name: ScreenPaths.manageProfiles, + builder: (context, state) => const SignInScreen(), + ), + GoRoute( + path: ScreenPaths.contacts, + name: ScreenPaths.contacts, + pageBuilder: (context, state) => NoTransitionPage( + name: ScreenPaths.contacts, + child: const ContactsScreen(), + ), + ), + GoRoute( + path: '/connect/:id', + name: ScreenPaths.connectHandle, + builder: (context, state) => + FollowRequestAdjudicationScreen(userId: state.pathParameters['id']!), + ), + GoRoute( + path: ScreenPaths.timelines, + name: ScreenPaths.timelines, + pageBuilder: (context, state) => NoTransitionPage( + name: ScreenPaths.timelines, + child: const HomeScreen(), + ), + ), + GoRoute( + path: ScreenPaths.messages, + name: ScreenPaths.messages, + pageBuilder: (context, state) => const NoTransitionPage( + child: MessagesScreen(), + ), + routes: [ GoRoute( - path: '/connect/:id', - name: ScreenPaths.connectHandle, - builder: (context, state) => FollowRequestAdjudicationScreen( - userId: state.pathParameters['id']!), - ), - GoRoute( - path: ScreenPaths.timelines, - name: ScreenPaths.timelines, + path: 'new_thread', pageBuilder: (context, state) => NoTransitionPage( - name: ScreenPaths.timelines, - child: const HomeScreen(), + child: MessagesNewThread(), + ), + ), + ], + ), + GoRoute( + name: ScreenPaths.thread, + path: ScreenPaths.thread, + builder: (context, state) => + MessageThreadScreen(parentThreadId: state.uri.queryParameters['uri']!), + ), + GoRoute( + name: ScreenPaths.circleManagement, + path: ScreenPaths.circleManagement, + builder: (context, state) => const CircleManagementScreen(), + routes: [ + GoRoute( + path: 'show/:id', + builder: (context, state) => CircleEditorScreen( + circleId: state.pathParameters['id']!, ), ), GoRoute( - path: ScreenPaths.messages, - name: ScreenPaths.messages, - pageBuilder: (context, state) => const NoTransitionPage( - child: MessagesScreen(), - ), - routes: [ - GoRoute( - path: 'new_thread', - pageBuilder: (context, state) => NoTransitionPage( - child: MessagesNewThread(), - ), - ), - ], + path: 'new', + builder: (context, state) => const CircleCreateScreen(), ), GoRoute( - name: ScreenPaths.thread, - path: ScreenPaths.thread, - builder: (context, state) => MessageThreadScreen( - parentThreadId: state.uri.queryParameters['uri']!), - ), - GoRoute( - name: ScreenPaths.circleManagement, - path: ScreenPaths.circleManagement, - builder: (context, state) => const CircleManagementScreen(), - routes: [ - GoRoute( - path: 'show/:id', - builder: (context, state) => CircleEditorScreen( - circleId: state.pathParameters['id']!, - ), - ), - GoRoute( - path: 'new', - builder: (context, state) => const CircleCreateScreen(), - ), - GoRoute( - path: 'add_users/:id', - builder: (context, state) => - CircleAddUsersScreen(circleId: state.pathParameters['id']!), - ), - ], - ), - GoRoute( - path: ScreenPaths.settings, - name: ScreenPaths.settings, - pageBuilder: (context, state) => const NoTransitionPage( - child: SettingsScreen(), - ), - ), - GoRoute( - path: ScreenPaths.gallery, - name: ScreenPaths.gallery, - pageBuilder: (context, state) => const NoTransitionPage( - child: GalleryBrowsersScreen(), - ), - routes: [ - GoRoute( - path: 'show', - builder: (context, state) => GalleryScreen( - galleryName: state.extra!.toString(), - ), - ), - GoRoute( - path: 'edit/:name/image/:id', - builder: (context, state) => ImageEditorScreen( - galleryName: state.pathParameters['name']!, - imageId: state.pathParameters['id']!, - ), - ), - ], - ), - GoRoute( - path: ScreenPaths.notifications, - name: ScreenPaths.notifications, - pageBuilder: (context, state) => const NoTransitionPage( - child: NotificationsScreen(), - ), - ), - GoRoute( - path: ScreenPaths.splash, - name: ScreenPaths.splash, - builder: (context, state) => const SplashScreen(), - ), - GoRoute( - path: '/post', - redirect: (context, state) { - if (state.uri.toString() == '/post') { - return '/post/new'; - } - - return null; - }, - routes: [ - GoRoute( - path: 'new', - builder: (context, state) => const EditorScreen( - forEditing: false, - ), - ), - GoRoute( - path: 'edit/:id', - builder: (context, state) => EditorScreen( - id: state.pathParameters['id'] ?? 'Not Found', - forEditing: true, - ), - ), - GoRoute( - path: 'view/:id/:goto_id', - builder: (context, state) => PostScreen( - id: state.pathParameters['id'] ?? 'Not Found', - goToId: state.pathParameters['goto_id'] ?? 'Not Found', - ), - ), - ]), - GoRoute( - path: '/comment', - redirect: (context, state) { - if (state.uri.toString() == '/comment') { - return '/comment/new'; - } - - return null; - }, - routes: [ - GoRoute( - path: 'new', - builder: (context, state) => EditorScreen( - parentId: state.uri.queryParameters['parent_id'] ?? '', - forEditing: false, - ), - ), - GoRoute( - path: 'edit/:id', - builder: (context, state) => EditorScreen( - id: state.pathParameters['id'] ?? 'Not Found', - forEditing: true, - ), - ), - ]), - GoRoute( - path: '/user_posts/:id', - name: ScreenPaths.userPosts, + path: 'add_users/:id', builder: (context, state) => - UserPostsScreen(userId: state.pathParameters['id']!), + CircleAddUsersScreen(circleId: state.pathParameters['id']!), ), + ], + ), + GoRoute( + path: ScreenPaths.settings, + name: ScreenPaths.settings, + pageBuilder: (context, state) => const NoTransitionPage( + child: SettingsScreen(), + ), + ), + GoRoute( + path: ScreenPaths.gallery, + name: ScreenPaths.gallery, + pageBuilder: (context, state) => const NoTransitionPage( + child: GalleryBrowsersScreen(), + ), + routes: [ GoRoute( - path: '/likes/:id', - name: ScreenPaths.likes, - builder: (context, state) => InteractionsViewerScreen( - statusId: state.pathParameters['id']!, - type: InteractionType.like, + path: 'show', + builder: (context, state) => GalleryScreen( + galleryName: state.extra!.toString(), ), ), GoRoute( - path: '/reshares/:id', - name: ScreenPaths.reshares, - builder: (context, state) => InteractionsViewerScreen( - statusId: state.pathParameters['id']!, - type: InteractionType.reshare, + path: 'edit/:name/image/:id', + builder: (context, state) => ImageEditorScreen( + galleryName: state.pathParameters['name']!, + imageId: state.pathParameters['id']!, ), ), - GoRoute( - path: '/user_profile/:id', - name: ScreenPaths.userProfile, - builder: (context, state) => - UserProfileScreen(userId: state.pathParameters['id']!), - ), - GoRoute( - path: ScreenPaths.search, - name: ScreenPaths.search, - pageBuilder: (context, state) => NoTransitionPage( - name: ScreenPaths.search, - child: const SearchScreen(), + ], + ), + GoRoute( + path: ScreenPaths.notifications, + name: ScreenPaths.notifications, + pageBuilder: (context, state) => const NoTransitionPage( + child: NotificationsScreen(), + ), + ), + GoRoute( + path: ScreenPaths.splash, + name: ScreenPaths.splash, + builder: (context, state) => const SplashScreen(), + ), + GoRoute( + path: '/post', + redirect: (context, state) { + if (state.uri.toString() == '/post') { + return '/post/new'; + } + + return null; + }, + routes: [ + GoRoute( + path: 'new', + builder: (context, state) => const EditorScreen( + forEditing: false, + ), ), - ), - GoRoute( - path: ScreenPaths.logViewer, - name: ScreenPaths.logViewer, - pageBuilder: (context, state) => const NoTransitionPage( - child: LogViewerScreen(), + GoRoute( + path: 'edit/:id', + builder: (context, state) => EditorScreen( + id: state.pathParameters['id'] ?? 'Not Found', + forEditing: true, + ), ), - ), - ]); + GoRoute( + path: 'view/:id/:goto_id', + builder: (context, state) => PostScreen( + id: state.pathParameters['id'] ?? 'Not Found', + goToId: state.pathParameters['goto_id'] ?? 'Not Found', + ), + ), + ]), + GoRoute( + path: '/comment', + redirect: (context, state) { + if (state.uri.toString() == '/comment') { + return '/comment/new'; + } + + return null; + }, + routes: [ + GoRoute( + path: 'new', + builder: (context, state) => EditorScreen( + parentId: state.uri.queryParameters['parent_id'] ?? '', + forEditing: false, + ), + ), + GoRoute( + path: 'edit/:id', + builder: (context, state) => EditorScreen( + id: state.pathParameters['id'] ?? 'Not Found', + forEditing: true, + ), + ), + ]), + GoRoute( + path: '/user_posts/:id', + name: ScreenPaths.userPosts, + builder: (context, state) => + UserPostsScreen(userId: state.pathParameters['id']!), + ), + GoRoute( + path: '/likes/:id', + name: ScreenPaths.likes, + builder: (context, state) => InteractionsViewerScreen( + statusId: state.pathParameters['id']!, + type: InteractionType.like, + ), + ), + GoRoute( + path: '/reshares/:id', + name: ScreenPaths.reshares, + builder: (context, state) => InteractionsViewerScreen( + statusId: state.pathParameters['id']!, + type: InteractionType.reshare, + ), + ), + GoRoute( + path: '/user_profile/:id', + name: ScreenPaths.userProfile, + builder: (context, state) => + UserProfileScreen(userId: state.pathParameters['id']!), + ), + GoRoute( + path: ScreenPaths.search, + name: ScreenPaths.search, + pageBuilder: (context, state) => NoTransitionPage( + name: ScreenPaths.search, + child: const SearchScreen(), + ), + ), + GoRoute( + path: ScreenPaths.logViewer, + name: ScreenPaths.logViewer, + pageBuilder: (context, state) => const NoTransitionPage( + child: LogViewerScreen(), + ), + ), +]; diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 82fb0d3..b847825 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:relatica/controls/focus_mode_status_headline.dart'; +import 'package:relatica/riverpod_controllers/focus_mode.dart'; import '../controls/app_bottom_nav_bar.dart'; import '../controls/linear_status_indicator.dart'; @@ -16,14 +19,14 @@ import '../services/network_status_service.dart'; import '../services/timeline_manager.dart'; import '../utils/active_profile_selector.dart'; -class HomeScreen extends StatefulWidget { +class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @override - State createState() => _HomeScreenState(); + ConsumerState createState() => _HomeScreenState(); } -class _HomeScreenState extends State { +class _HomeScreenState extends ConsumerState { final _logger = Logger('$HomeScreen'); TimelineIdentifiers currentTimeline = TimelineIdentifiers.home(); @@ -49,8 +52,12 @@ class _HomeScreenState extends State { _logger.finest('Build'); final accountService = getIt(); final nss = getIt(); + final focusMode = ref.watch(focusModeProvider); - final timeline = TimelinePanel(timeline: currentTimeline); + final timeline = TimelinePanel( + timeline: focusMode.enabled + ? TimelineIdentifiers.myPosts() + : currentTimeline); return Scaffold( appBar: AppBar( @@ -65,7 +72,9 @@ class _HomeScreenState extends State { }) : null, backgroundColor: Theme.of(context).canvasColor, - title: buildTimelineSelector(context), + title: focusMode.enabled + ? AppBottomFocusModeStatus(disableTime: focusMode.disableTime) + : buildTimelineSelector(context), ), body: Center( child: Column( @@ -78,10 +87,12 @@ class _HomeScreenState extends State { ), ), drawer: const StandardAppDrawer(), - bottomNavigationBar: AppBottomNavBar( - currentButton: NavBarButtons.timelines, - onHomeButtonReclick: () => timeline.scrollToTop(), - ), + bottomNavigationBar: focusMode.enabled + ? null + : AppBottomNavBar( + currentButton: NavBarButtons.timelines, + onHomeButtonReclick: () => timeline.scrollToTop(), + ), floatingActionButton: FloatingActionButton.small( onPressed: () { context.push('/post/new');