From 6d28ee51dd382e6c9f3c392e7164cbb9422bf643 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 19 Aug 2022 19:40:55 +0200 Subject: [PATCH] created help sheets; improvements & bugfixes --- lib/constants/help_sheet_id.dart | 4 + lib/constants/storage_keys.dart | 1 + lib/locale/l10n/app_en.arb | 12 + lib/main.dart | 4 +- lib/managers/cache_manager.dart | 14 +- lib/managers/user_help_sheets_manager.dart | 27 ++ lib/screens/login_screen.dart | 9 +- lib/screens/main_screen.dart | 388 +++++++++--------- .../main_screen/camera_help_content.dart | 60 +++ lib/screens/server_loading_screen.dart | 22 +- lib/screens/timeline_screen.dart | 59 +-- .../timeline_help_content.dart | 41 ++ lib/screens/welcome_screen.dart | 8 + lib/utils/loadable.dart | 8 +- lib/widgets/help_content_text.dart | 34 ++ lib/widgets/help_sheet.dart | 117 ++++++ lib/widgets/help_sheet/help_sheet_form.dart | 67 +++ lib/widgets/modal_sheet.dart | 49 ++- 18 files changed, 679 insertions(+), 245 deletions(-) create mode 100644 lib/constants/help_sheet_id.dart create mode 100644 lib/managers/user_help_sheets_manager.dart create mode 100644 lib/screens/main_screen/camera_help_content.dart create mode 100644 lib/screens/timeline_screen/timeline_help_content.dart create mode 100644 lib/widgets/help_content_text.dart create mode 100644 lib/widgets/help_sheet.dart create mode 100644 lib/widgets/help_sheet/help_sheet_form.dart diff --git a/lib/constants/help_sheet_id.dart b/lib/constants/help_sheet_id.dart new file mode 100644 index 0000000..749c838 --- /dev/null +++ b/lib/constants/help_sheet_id.dart @@ -0,0 +1,4 @@ +enum HelpSheetID { + mainScreen, + timelineScreen, +} diff --git a/lib/constants/storage_keys.dart b/lib/constants/storage_keys.dart index 143255e..380787d 100644 --- a/lib/constants/storage_keys.dart +++ b/lib/constants/storage_keys.dart @@ -1,2 +1,3 @@ const CACHE_KEY = '_cache'; const SETTINGS_KEY = 'settings'; +const USER_HELP_SHEETS_KEY = 'user_help_sheets'; diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb index 6b7b278..595645a 100644 --- a/lib/locale/l10n/app_en.arb +++ b/lib/locale/l10n/app_en.arb @@ -4,6 +4,7 @@ "generalError": "There was an error", "generalCancelButtonLabel": "Cancel", "generalContinueButtonLabel": "Continue", + "generalUnderstoodButtonLabel": "OK", "welcomeScreenDescription": "Find out what you did all the days and unlock moments you completely forgot!", "welcomeScreenSubtitle": "What did I do today?", @@ -46,6 +47,10 @@ "mainScreenTakeVideoActionSaveVideo": "Taking video, please hold still...", "mainScreenTakeVideoActionUploadingVideo": "Uploading video...", + "mainScreenHelpSheetTitle": "Record your best moments in life", + "mainScreenHelpSheetTakePhotoExplanation": "Tap on the shutter button once to take a photo.", + "mainScreenHelpSheetTakeVideoExplanation": "Hold down the shutter button to start recording a video. Leave the button to stop recording.", + "recordingOverlayIsRecording": "Recording", @@ -153,6 +158,13 @@ "timelineScreenTitle": "Timeline", + "timelineScreenHelpSheetTitle": "Welcome to your timeline", + "timelineHelpContentDescription": "Your memories are displayed in chronological order. You can swipe left or right to navigate through the memories of the given day. You can also swipe up or down to navigate through the days of your memories. The timeline automatically progresses through your memories.", + "timelineHelpContentHoldDownExplanation": "Hold down to pause the timeline from progressing to your next memory.", + "timelineHelpContentTapTwiceExplanation": "Tap twice to see more details about your memory.", + + + "helpSheetDontShowAgain": "Don't show this help sheet again", "enumMapping_ResolutionPreset_low": "Low", diff --git a/lib/main.dart b/lib/main.dart index 2a769b7..f49192b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -85,7 +85,9 @@ class _MyAppState extends State { TimelineScreen.ID: (context) => const TimelineScreen(), GrantPermissionScreen.ID: (context) => const GrantPermissionScreen(), CalendarScreen.ID: (context) => const CalendarScreen(), - ServerLoadingScreen.ID: (context) => const ServerLoadingScreen(), + ServerLoadingScreen.ID: (context) => const ServerLoadingScreen( + isInitialLoading: true, + ), EmptyScreen.ID: (context) => const EmptyScreen(), SettingsScreen.ID: (context) => const SettingsScreen(), }, diff --git a/lib/managers/cache_manager.dart b/lib/managers/cache_manager.dart index 69baaaa..6b8a004 100644 --- a/lib/managers/cache_manager.dart +++ b/lib/managers/cache_manager.dart @@ -14,12 +14,16 @@ class CacheManager { final existingEntry = await storage.read(key: cacheKey); if (existingEntry != null) { - final entry = jsonDecode(existingEntry); - final DateTime creationDate = DateTime.parse(entry['creationDate']); + try { + final entry = jsonDecode(existingEntry); + final DateTime creationDate = DateTime.parse(entry['creationDate']); - // Check if the entry is still valid using CACHE_INVALIDATION_DURATION as the validity duration. - return DateTime.now().difference(creationDate) < - CACHE_INVALIDATION_DURATION; + // Check if the entry is still valid using CACHE_INVALIDATION_DURATION as the validity duration. + return DateTime.now().difference(creationDate) < + CACHE_INVALIDATION_DURATION; + } catch (_) { + return false; + } } return false; diff --git a/lib/managers/user_help_sheets_manager.dart b/lib/managers/user_help_sheets_manager.dart new file mode 100644 index 0000000..41c7772 --- /dev/null +++ b/lib/managers/user_help_sheets_manager.dart @@ -0,0 +1,27 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:quid_faciam_hodie/constants/help_sheet_id.dart'; +import 'package:quid_faciam_hodie/constants/storage_keys.dart'; + +const storage = FlutterSecureStorage(); + +class UserHelpSheetsManager { + static String _createKey(final HelpSheetID helpID) => + '$USER_HELP_SHEETS_KEY/$helpID'; + + static Future getIfAlreadyShown(final HelpSheetID helpID) async => + (await storage.read(key: _createKey(helpID))) == 'true'; + + static Future setAsShown(final HelpSheetID helpID) async => + storage.write( + key: _createKey(helpID), + value: 'true', + ); + + static Future deleteAll() async { + const keys = HelpSheetID.values; + + for (final key in keys) { + await storage.delete(key: _createKey(key)); + } + } +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 50dbbdd..2cea031 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -94,7 +94,14 @@ class _LoginScreenState extends AuthState with Loadable { } if (mounted) { - Navigator.pushReplacementNamed(context, ServerLoadingScreen.ID); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ServerLoadingScreen( + nextScreen: MainScreen.ID, + ), + ), + ); } } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 14c302a..9278fb5 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -1,25 +1,30 @@ +import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:location/location.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:quid_faciam_hodie/constants/help_sheet_id.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/constants/values.dart'; import 'package:quid_faciam_hodie/extensions/snackbar.dart'; import 'package:quid_faciam_hodie/managers/file_manager.dart'; import 'package:quid_faciam_hodie/managers/global_values_manager.dart'; +import 'package:quid_faciam_hodie/screens/main_screen/camera_help_content.dart'; import 'package:quid_faciam_hodie/screens/main_screen/settings_button_overlay.dart'; import 'package:quid_faciam_hodie/utils/auth_required.dart'; import 'package:quid_faciam_hodie/utils/loadable.dart'; import 'package:quid_faciam_hodie/utils/tag_location_to_image.dart'; import 'package:quid_faciam_hodie/widgets/animate_in_builder.dart'; import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart'; +import 'package:quid_faciam_hodie/widgets/help_sheet.dart'; import 'package:quid_faciam_hodie/widgets/icon_button_child.dart'; import 'package:quid_faciam_hodie/widgets/sheet_indicator.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -330,211 +335,218 @@ class _MainScreenState extends AuthRequiredState with Loadable { ); } - return Container( - color: Colors.black, - child: ExpandableBottomSheet( - background: SafeArea( - child: Align( - alignment: Alignment.topCenter, - child: AnimateInBuilder( - builder: (showPreview) => AnimatedOpacity( - opacity: showPreview ? 1.0 : 0.0, - duration: const Duration(milliseconds: 1100), - curve: Curves.easeOutQuad, - child: ClipRRect( - borderRadius: BorderRadius.circular(SMALL_SPACE), - child: AspectRatio( - aspectRatio: 1 / controller!.value.aspectRatio, - child: Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - controller!.buildPreview(), - if (isRecording) - RecordingOverlay(controller: controller!), - if (!isRecording) SettingsButtonOverlay(), - if (uploadingPhotoAnimation != null) - UploadingPhoto( - data: uploadingPhotoAnimation!, - ), - ], + return HelpSheet( + title: localizations.mainScreenHelpSheetTitle, + helpContent: const CameraHelpContent(), + helpID: HelpSheetID.mainScreen, + child: Container( + color: Colors.black, + child: ExpandableBottomSheet( + background: SafeArea( + child: Align( + alignment: Alignment.topCenter, + child: AnimateInBuilder( + builder: (showPreview) => AnimatedOpacity( + opacity: showPreview ? 1.0 : 0.0, + duration: const Duration(milliseconds: 1100), + curve: Curves.easeOutQuad, + child: ClipRRect( + borderRadius: BorderRadius.circular(SMALL_SPACE), + child: AspectRatio( + aspectRatio: 1 / controller!.value.aspectRatio, + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + controller!.buildPreview(), + if (isRecording) + RecordingOverlay(controller: controller!), + if (!isRecording) SettingsButtonOverlay(), + if (uploadingPhotoAnimation != null) + UploadingPhoto( + data: uploadingPhotoAnimation!, + ), + ], + ), ), ), ), ), ), ), - ), - persistentHeader: Container( - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(LARGE_SPACE), - topRight: Radius.circular(LARGE_SPACE), - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.symmetric( - vertical: MEDIUM_SPACE, - horizontal: MEDIUM_SPACE, - ), - child: SheetIndicator(), + persistentHeader: Container( + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), ), - Padding( - padding: - const EdgeInsets.symmetric(vertical: SMALL_SPACE), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: FadeAndMoveInAnimation( - translationDuration: - DEFAULT_TRANSLATION_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - opacityDuration: DEFAULT_OPACITY_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - child: ChangeCameraButton( - disabled: lockCamera || isRecording, - onChangeCamera: () { - final currentCameraIndex = GlobalValuesManager - .cameras - .indexOf(controller!.description); - final availableCameras = - GlobalValuesManager.cameras.length; - - onNewCameraSelected( - GlobalValuesManager.cameras[ - (currentCameraIndex + 1) % - availableCameras], - ); - }, - ), - ), - ), - Expanded( - child: FadeAndMoveInAnimation( - child: RecordButton( - disabled: lockCamera, - active: isRecording, - onVideoBegin: () async { - setState(() { - isRecording = true; - }); - - if (controller!.value.isRecordingVideo) { - // A recording has already started, do nothing. - return; - } - - await controller!.startVideoRecording(); - }, - onVideoEnd: takeVideo, - onPhotoShot: takePhoto, - ), - ), - ), - Expanded( - child: FadeAndMoveInAnimation( - translationDuration: - DEFAULT_TRANSLATION_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - opacityDuration: DEFAULT_OPACITY_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - child: TodayPhotoButton( - onLeave: () { - controller!.setFlashMode(FlashMode.off); - }, - onComeBack: () { - if (isTorchEnabled) { - controller!.setFlashMode(FlashMode.torch); - } - }, - ), - ), - ), - ], - ), - ), - ], - ), - ), - expandableContent: Container( - color: Colors.black, - child: Padding( - padding: const EdgeInsets.only( - left: LARGE_SPACE, - right: LARGE_SPACE, - bottom: MEDIUM_SPACE, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.resolveWith( - (_) => isTorchEnabled ? Colors.white : Colors.black, - ), - foregroundColor: - MaterialStateProperty.resolveWith( - (_) => isTorchEnabled ? Colors.black : Colors.white, - ), - ), - onPressed: () { - setState(() { - isTorchEnabled = !isTorchEnabled; - - if (isTorchEnabled) { - controller!.setFlashMode(FlashMode.torch); - } else { - controller!.setFlashMode(FlashMode.off); - } - }); - }, - child: IconButtonChild( - icon: const Icon(Icons.flashlight_on_rounded), - label: - Text(localizations.mainScreenActionsTorchButton), + const Padding( + padding: EdgeInsets.symmetric( + vertical: MEDIUM_SPACE, + horizontal: MEDIUM_SPACE, ), + child: SheetIndicator(), ), - ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.resolveWith( - (_) => Colors.white10, - ), - foregroundColor: - MaterialStateProperty.resolveWith( - (_) => Colors.white, - ), - ), - onPressed: zoomLevels == null - ? null - : () { - final newZoomLevelIndex = - ((currentZoomLevelIndex + 1) % - zoomLevels!.length); + Padding( + padding: + const EdgeInsets.symmetric(vertical: SMALL_SPACE), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: FadeAndMoveInAnimation( + translationDuration: + DEFAULT_TRANSLATION_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + opacityDuration: DEFAULT_OPACITY_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + child: ChangeCameraButton( + disabled: lockCamera || isRecording, + onChangeCamera: () { + final currentCameraIndex = + GlobalValuesManager.cameras + .indexOf(controller!.description); + final availableCameras = + GlobalValuesManager.cameras.length; - controller!.setZoomLevel( - zoomLevels![newZoomLevelIndex]); - - setState(() { - currentZoomLevelIndex = newZoomLevelIndex; - }); - }, - child: zoomLevels == null - ? Text(formatZoomLevel(1.0)) - : Text( - formatZoomLevel(currentZoomLevel), + onNewCameraSelected( + GlobalValuesManager.cameras[ + (currentCameraIndex + 1) % + availableCameras], + ); + }, + ), ), + ), + Expanded( + child: FadeAndMoveInAnimation( + child: RecordButton( + disabled: lockCamera, + active: isRecording, + onVideoBegin: () async { + setState(() { + isRecording = true; + }); + + if (controller!.value.isRecordingVideo) { + // A recording has already started, do nothing. + return; + } + + await controller!.startVideoRecording(); + }, + onVideoEnd: takeVideo, + onPhotoShot: takePhoto, + ), + ), + ), + Expanded( + child: FadeAndMoveInAnimation( + translationDuration: + DEFAULT_TRANSLATION_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + opacityDuration: DEFAULT_OPACITY_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + child: TodayPhotoButton( + onLeave: () { + controller!.setFlashMode(FlashMode.off); + }, + onComeBack: () { + if (isTorchEnabled) { + controller!.setFlashMode(FlashMode.torch); + } + }, + ), + ), + ), + ], + ), ), ], ), ), + expandableContent: Container( + color: Colors.black, + child: Padding( + padding: const EdgeInsets.only( + left: LARGE_SPACE, + right: LARGE_SPACE, + bottom: MEDIUM_SPACE, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.resolveWith( + (_) => + isTorchEnabled ? Colors.white : Colors.black, + ), + foregroundColor: + MaterialStateProperty.resolveWith( + (_) => + isTorchEnabled ? Colors.black : Colors.white, + ), + ), + onPressed: () { + setState(() { + isTorchEnabled = !isTorchEnabled; + + if (isTorchEnabled) { + controller!.setFlashMode(FlashMode.torch); + } else { + controller!.setFlashMode(FlashMode.off); + } + }); + }, + child: IconButtonChild( + icon: const Icon(Icons.flashlight_on_rounded), + label: Text( + localizations.mainScreenActionsTorchButton), + ), + ), + ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.resolveWith( + (_) => Colors.white10, + ), + foregroundColor: + MaterialStateProperty.resolveWith( + (_) => Colors.white, + ), + ), + onPressed: zoomLevels == null + ? null + : () { + final newZoomLevelIndex = + ((currentZoomLevelIndex + 1) % + zoomLevels!.length); + + controller!.setZoomLevel( + zoomLevels![newZoomLevelIndex]); + + setState(() { + currentZoomLevelIndex = newZoomLevelIndex; + }); + }, + child: zoomLevels == null + ? Text(formatZoomLevel(1.0)) + : Text( + formatZoomLevel(currentZoomLevel), + ), + ), + ], + ), + ), + ), ), ), ); diff --git a/lib/screens/main_screen/camera_help_content.dart b/lib/screens/main_screen/camera_help_content.dart new file mode 100644 index 0000000..04dc276 --- /dev/null +++ b/lib/screens/main_screen/camera_help_content.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; +import 'package:quid_faciam_hodie/widgets/help_content_text.dart'; + +class CameraHelpContent extends StatelessWidget { + const CameraHelpContent({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final iconColor = getBodyTextColor(context).withOpacity(.2); + + return Column( + children: [ + HelpContentText( + icon: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.circle_rounded, + color: iconColor, + ), + const Icon( + Icons.circle_rounded, + color: Colors.white, + size: 13, + ), + ], + ), + text: localizations.mainScreenHelpSheetTakePhotoExplanation, + ), + const SizedBox(height: MEDIUM_SPACE), + HelpContentText( + icon: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.circle_rounded, + color: iconColor, + ), + Icon( + Icons.circle_rounded, + color: Colors.white, + size: 19, + ), + Icon( + Icons.square_rounded, + color: Colors.red, + size: 10, + ), + ], + ), + text: localizations.mainScreenHelpSheetTakeVideoExplanation, + ), + ], + ); + } +} diff --git a/lib/screens/server_loading_screen.dart b/lib/screens/server_loading_screen.dart index 775434f..76728b1 100644 --- a/lib/screens/server_loading_screen.dart +++ b/lib/screens/server_loading_screen.dart @@ -5,11 +5,11 @@ import 'package:provider/provider.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/managers/global_values_manager.dart'; import 'package:quid_faciam_hodie/models/memories.dart'; -import 'package:quid_faciam_hodie/screens/grant_permission_screen.dart'; import 'package:quid_faciam_hodie/screens/login_screen.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'empty_screen.dart'; +import 'grant_permission_screen.dart'; import 'main_screen.dart'; import 'server_loading_screen/dot_animation.dart'; import 'welcome_screen.dart'; @@ -18,10 +18,12 @@ class ServerLoadingScreen extends StatefulWidget { static const ID = '/'; final String? nextScreen; + final bool isInitialLoading; const ServerLoadingScreen({ Key? key, this.nextScreen, + this.isInitialLoading = false, }) : super(key: key); @override @@ -33,15 +35,27 @@ class _ServerLoadingScreenState extends State { void initState() { super.initState(); - load(); + WidgetsBinding.instance.addPostFrameCallback((_) { + load(); + }); } Future load() async { - while (!(await GlobalValuesManager.hasGrantedPermissions())) { + if (widget.isInitialLoading) { await Navigator.pushNamed( context, - GrantPermissionScreen.ID, + WelcomeScreen.ID, ); + return; + } + + if (widget.nextScreen != WelcomeScreen.ID) { + while (!(await GlobalValuesManager.hasGrantedPermissions())) { + await Navigator.pushNamed( + context, + GrantPermissionScreen.ID, + ); + } } await GlobalValuesManager.waitForInitialization(); diff --git a/lib/screens/timeline_screen.dart b/lib/screens/timeline_screen.dart index e4030b7..a495ed8 100644 --- a/lib/screens/timeline_screen.dart +++ b/lib/screens/timeline_screen.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:provider/provider.dart'; +import 'package:quid_faciam_hodie/constants/help_sheet_id.dart'; import 'package:quid_faciam_hodie/extensions/date.dart'; import 'package:quid_faciam_hodie/models/memories.dart'; import 'package:quid_faciam_hodie/models/timeline.dart'; +import 'package:quid_faciam_hodie/screens/timeline_screen/timeline_help_content.dart'; import 'package:quid_faciam_hodie/utils/loadable.dart'; +import 'package:quid_faciam_hodie/widgets/help_sheet.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'calendar_screen.dart'; import 'empty_screen.dart'; @@ -111,29 +114,39 @@ class _TimelineScreenState extends State with Loadable { return true; }, - child: PlatformScaffold( - appBar: isCupertino(context) - ? PlatformAppBar( - title: Text(localizations.timelineScreenTitle), - ) - : null, - body: ChangeNotifierProvider.value( - value: timeline, - child: PageView.builder( - controller: pageController, - scrollDirection: Axis.vertical, - itemCount: timeline.values.length, - onPageChanged: (newPage) { - if (timeline.currentIndex != newPage) { - // User manually changed page - timeline.setCurrentIndex(newPage); + child: HelpSheet( + title: localizations.timelineScreenHelpSheetTitle, + helpContent: const TimelineHelpContent(), + helpID: HelpSheetID.timelineScreen, + onSheetShown: timeline.pause, + onSheetHidden: () { + timeline.resume(); + print("dfsjnifksdf"); + }, + child: PlatformScaffold( + appBar: isCupertino(context) + ? PlatformAppBar( + title: Text(localizations.timelineScreenTitle), + ) + : null, + body: ChangeNotifierProvider.value( + value: timeline, + child: PageView.builder( + controller: pageController, + scrollDirection: Axis.vertical, + itemCount: timeline.values.length, + onPageChanged: (newPage) { + if (timeline.currentIndex != newPage) { + // User manually changed page + timeline.setCurrentIndex(newPage); - timeline.setMemoryIndex(0); - } - }, - itemBuilder: (_, index) => TimelinePage( - date: timeline.dateAtIndex(index), - memories: timeline.atIndex(index), + timeline.setMemoryIndex(0); + } + }, + itemBuilder: (_, index) => TimelinePage( + date: timeline.dateAtIndex(index), + memories: timeline.atIndex(index), + ), ), ), ), diff --git a/lib/screens/timeline_screen/timeline_help_content.dart b/lib/screens/timeline_screen/timeline_help_content.dart new file mode 100644 index 0000000..83f1713 --- /dev/null +++ b/lib/screens/timeline_screen/timeline_help_content.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; +import 'package:quid_faciam_hodie/widgets/help_content_text.dart'; + +class TimelineHelpContent extends StatelessWidget { + const TimelineHelpContent({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final textColor = getBodyTextColor(context); + + return Column( + children: [ + Text( + localizations.timelineHelpContentDescription, + textAlign: TextAlign.center, + style: getBodyTextTextStyle(context), + ), + const SizedBox(height: LARGE_SPACE), + HelpContentText( + icon: Icon( + Icons.touch_app_rounded, + color: textColor, + ), + text: localizations.timelineHelpContentHoldDownExplanation, + ), + const SizedBox(height: MEDIUM_SPACE), + HelpContentText( + icon: Icon( + Icons.align_vertical_bottom_sharp, + color: textColor, + ), + text: localizations.timelineHelpContentTapTwiceExplanation, + ), + ], + ); + } +} diff --git a/lib/screens/welcome_screen.dart b/lib/screens/welcome_screen.dart index 860363d..56e24e9 100644 --- a/lib/screens/welcome_screen.dart +++ b/lib/screens/welcome_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/managers/user_help_sheets_manager.dart'; import 'welcome_screen/pages/get_started_page.dart'; import 'welcome_screen/pages/guide_page.dart'; @@ -19,6 +20,13 @@ class WelcomeScreen extends StatefulWidget { class _WelcomeScreenState extends State { final controller = PageController(); + @override + void initState() { + super.initState(); + + UserHelpSheetsManager.deleteAll(); + } + @override void dispose() { controller.dispose(); diff --git a/lib/utils/loadable.dart b/lib/utils/loadable.dart index 496f5d6..7a0114e 100644 --- a/lib/utils/loadable.dart +++ b/lib/utils/loadable.dart @@ -24,9 +24,11 @@ mixin Loadable { try { await callback(); } finally { - setState(() { - _IDs.remove(id ?? _generalLoadingID); - }); + try { + setState(() { + _IDs.remove(id ?? _generalLoadingID); + }); + } catch (error) {} } } } diff --git a/lib/widgets/help_content_text.dart b/lib/widgets/help_content_text.dart new file mode 100644 index 0000000..43eb787 --- /dev/null +++ b/lib/widgets/help_content_text.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; + +class HelpContentText extends StatelessWidget { + final String text; + final Widget icon; + + const HelpContentText({ + Key? key, + required this.text, + required this.icon, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 3, + child: icon, + ), + const Spacer(flex: 1), + Expanded( + flex: 8, + child: Text( + text, + style: getBodyTextTextStyle(context), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/help_sheet.dart b/lib/widgets/help_sheet.dart new file mode 100644 index 0000000..3bcb160 --- /dev/null +++ b/lib/widgets/help_sheet.dart @@ -0,0 +1,117 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:quid_faciam_hodie/constants/help_sheet_id.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/managers/user_help_sheets_manager.dart'; +import 'package:quid_faciam_hodie/widgets/help_sheet/help_sheet_form.dart'; + +class HelpSheet extends StatefulWidget { + final Widget child; + final String title; + final Widget helpContent; + final HelpSheetID helpID; + final bool forceShow; + final VoidCallback? onSheetShown; + final VoidCallback? onSheetHidden; + + const HelpSheet({ + Key? key, + required this.child, + required this.title, + required this.helpContent, + required this.helpID, + this.forceShow = false, + this.onSheetShown, + this.onSheetHidden, + }) : super(key: key); + + @override + State createState() => _HelpSheetState(); +} + +class _HelpSheetState extends State { + bool isShowingSheet = false; + + @override + void initState() { + super.initState(); + + if (widget.forceShow) { + showSheet(); + } else { + checkIfSheetShouldBeShown(); + } + } + + void showSheet() { + if (isShowingSheet) { + return; + } + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) { + return; + } + + setState(() { + isShowingSheet = true; + }); + + if (widget.onSheetShown != null) { + widget.onSheetShown!(); + } + + final dontShowSheetAgain = await showPlatformModalSheet( + material: MaterialModalSheetData( + isDismissible: false, + isScrollControlled: true, + backgroundColor: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), + ), + ), + ), + context: context, + builder: (_) => HelpSheetForm( + helpContent: widget.helpContent, + title: widget.title, + ), + ); + + if (widget.onSheetHidden != null) { + widget.onSheetHidden!(); + } + + if (mounted) { + setState(() { + isShowingSheet = false; + }); + } + + if (dontShowSheetAgain) { + await UserHelpSheetsManager.setAsShown(widget.helpID); + } + }); + } + + Future checkIfSheetShouldBeShown() async { + final hasSheetBeShownAlready = + await UserHelpSheetsManager.getIfAlreadyShown(widget.helpID); + + if (!hasSheetBeShownAlready) { + showSheet(); + } + } + + @override + Widget build(BuildContext context) { + return AnimatedScale( + scale: isShowingSheet ? .99 : 1, + duration: const Duration(milliseconds: 200), + child: widget.child, + ); + } +} diff --git a/lib/widgets/help_sheet/help_sheet_form.dart b/lib/widgets/help_sheet/help_sheet_form.dart new file mode 100644 index 0000000..f89ff7f --- /dev/null +++ b/lib/widgets/help_sheet/help_sheet_form.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; +import 'package:quid_faciam_hodie/widgets/modal_sheet.dart'; + +class HelpSheetForm extends StatefulWidget { + final String title; + final Widget helpContent; + + const HelpSheetForm({ + Key? key, + required this.helpContent, + required this.title, + }) : super(key: key); + + @override + State createState() => _HelpSheetFormState(); +} + +class _HelpSheetFormState extends State { + bool dontShowSheetAgain = true; + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return ModalSheet( + child: Column( + children: [ + Text( + widget.title, + textAlign: TextAlign.center, + style: getTitleTextStyle(context), + ), + const SizedBox(height: MEDIUM_SPACE), + widget.helpContent, + const SizedBox(height: LARGE_SPACE), + PlatformElevatedButton( + child: Text(localizations.generalUnderstoodButtonLabel), + onPressed: () => Navigator.pop(context, dontShowSheetAgain), + ), + const SizedBox(height: MEDIUM_SPACE), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PlatformSwitch( + value: dontShowSheetAgain, + onChanged: (value) { + setState(() { + dontShowSheetAgain = value; + }); + }, + ), + const SizedBox(width: SMALL_SPACE), + Text( + localizations.helpSheetDontShowAgain, + style: getBodyTextTextStyle(context), + ) + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/modal_sheet.dart b/lib/widgets/modal_sheet.dart index 786ff5b..6d21947 100644 --- a/lib/widgets/modal_sheet.dart +++ b/lib/widgets/modal_sheet.dart @@ -1,6 +1,8 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; class ModalSheet extends StatelessWidget { final Widget child; @@ -12,30 +14,37 @@ class ModalSheet extends StatelessWidget { @override Widget build(BuildContext context) { - return Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Padding( - padding: - EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: Material( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(LARGE_SPACE), - topRight: Radius.circular(LARGE_SPACE), - ), - color: platformThemeData( - context, - material: (data) => - data.bottomSheetTheme.modalBackgroundColor ?? - data.bottomAppBarColor, - cupertino: (data) => data.barBackgroundColor, + final innerChild = Padding( + padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), + child: child, + ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformWidget( + material: (_, __) => Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), + ), + color: getSheetColor(context), ), + padding: const EdgeInsets.symmetric(vertical: MEDIUM_SPACE), + child: innerChild, + ), + cupertino: (_, __) => CupertinoPopupSurface( + isSurfacePainted: false, child: Container( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: child, + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE), + child: innerChild, + ), ), ), - ), + ) ], ); }