created help sheets; improvements & bugfixes

This commit is contained in:
Myzel394 2022-08-19 19:40:55 +02:00
parent 909d4f8d89
commit 6d28ee51dd
18 changed files with 679 additions and 245 deletions

View File

@ -0,0 +1,4 @@
enum HelpSheetID {
mainScreen,
timelineScreen,
}

View File

@ -1,2 +1,3 @@
const CACHE_KEY = '_cache';
const SETTINGS_KEY = 'settings';
const USER_HELP_SHEETS_KEY = 'user_help_sheets';

View File

@ -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",

View File

@ -85,7 +85,9 @@ class _MyAppState extends State<MyApp> {
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(),
},

View File

@ -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;

View File

@ -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<bool> getIfAlreadyShown(final HelpSheetID helpID) async =>
(await storage.read(key: _createKey(helpID))) == 'true';
static Future<void> setAsShown(final HelpSheetID helpID) async =>
storage.write(
key: _createKey(helpID),
value: 'true',
);
static Future<void> deleteAll() async {
const keys = HelpSheetID.values;
for (final key in keys) {
await storage.delete(key: _createKey(key));
}
}
}

View File

@ -94,7 +94,14 @@ class _LoginScreenState extends AuthState<LoginScreen> with Loadable {
}
if (mounted) {
Navigator.pushReplacementNamed(context, ServerLoadingScreen.ID);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ServerLoadingScreen(
nextScreen: MainScreen.ID,
),
),
);
}
}

View File

@ -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<MainScreen> 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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith<Color>(
(_) => isTorchEnabled ? Colors.white : Colors.black,
),
foregroundColor:
MaterialStateProperty.resolveWith<Color>(
(_) => 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<Color>(
(_) => Colors.white10,
),
foregroundColor:
MaterialStateProperty.resolveWith<Color>(
(_) => 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: <Widget>[
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: <Widget>[
ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith<Color>(
(_) =>
isTorchEnabled ? Colors.white : Colors.black,
),
foregroundColor:
MaterialStateProperty.resolveWith<Color>(
(_) =>
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<Color>(
(_) => Colors.white10,
),
foregroundColor:
MaterialStateProperty.resolveWith<Color>(
(_) => 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),
),
),
],
),
),
),
),
),
);

View File

@ -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: <Widget>[
HelpContentText(
icon: Stack(
alignment: Alignment.center,
children: <Widget>[
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: <Widget>[
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,
),
],
);
}
}

View File

@ -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<ServerLoadingScreen> {
void initState() {
super.initState();
load();
WidgetsBinding.instance.addPostFrameCallback((_) {
load();
});
}
Future<void> 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();

View File

@ -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<TimelineScreen> 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),
),
),
),
),

View File

@ -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: <Widget>[
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,
),
],
);
}
}

View File

@ -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<WelcomeScreen> {
final controller = PageController();
@override
void initState() {
super.initState();
UserHelpSheetsManager.deleteAll();
}
@override
void dispose() {
controller.dispose();

View File

@ -24,9 +24,11 @@ mixin Loadable {
try {
await callback();
} finally {
setState(() {
_IDs.remove(id ?? _generalLoadingID);
});
try {
setState(() {
_IDs.remove(id ?? _generalLoadingID);
});
} catch (error) {}
}
}
}

View File

@ -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: <Widget>[
Expanded(
flex: 3,
child: icon,
),
const Spacer(flex: 1),
Expanded(
flex: 8,
child: Text(
text,
style: getBodyTextTextStyle(context),
),
),
],
);
}
}

117
lib/widgets/help_sheet.dart Normal file
View File

@ -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<HelpSheet> createState() => _HelpSheetState();
}
class _HelpSheetState extends State<HelpSheet> {
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<void> 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,
);
}
}

View File

@ -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<HelpSheetForm> createState() => _HelpSheetFormState();
}
class _HelpSheetFormState extends State<HelpSheetForm> {
bool dontShowSheetAgain = true;
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return ModalSheet(
child: Column(
children: <Widget>[
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: <Widget>[
PlatformSwitch(
value: dontShowSheetAgain,
onChanged: (value) {
setState(() {
dontShowSheetAgain = value;
});
},
),
const SizedBox(width: SMALL_SPACE),
Text(
localizations.helpSheetDontShowAgain,
style: getBodyTextTextStyle(context),
)
],
),
],
),
);
}
}

View File

@ -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: <Widget>[
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,
),
),
),
),
)
],
);
}