improved welcome screen

This commit is contained in:
Myzel394 2022-08-18 16:32:08 +02:00
parent d17a62db90
commit 0a0e45bac2
18 changed files with 453 additions and 72 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -1,3 +1,26 @@
const SUPABASE_API_URL = 'https://gmqzelvauqziurlloawb.supabase.co';
const SUPABASE_API_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImdtcXplbHZhdXF6aXVybGxvYXdiIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjAzODE5MDcsImV4cCI6MTk3NTk1NzkwN30.D_964EIlD9WRFnG6MWtQtmIg04eMBbZhIEF7zl--bKw';
import 'dart:convert';
// Encode keys to base64 to avoid simple bots from scraping them
final SUPABASE_API_URL = String.fromCharCodes(
base64.decode(
'aHR0cHM6Ly9nbXF6ZWx' + '2YXVxeml1cmxsb2F3Yi5zdXBhYmFzZS5jbwo=',
),
);
final SUPABASE_API_KEY = String.fromCharCodes(
base64.decode(
'ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6' +
'WlNJc0luSmxaaUk2SW1kdGNYcGxiSFpoZFhGNmFYVnliR3h2WVhkaUlpd2ljbTlzWlNJNkltRnVi' +
'MjRpTENKcFlYUWlPakUyTmpBek9ERTVNRGNzSW1WNGNDSTZNVGszTlRrMU56a3dOMzAuRF85NjRF' +
'SWxEOVdSRm5HNk1XdFF0bUlnMDRlTUJiWmhJRUY3emwtLWJLdwo=',
),
);
final PEXELS_API_KEY = String.fromCharCodes(
base64.decode(
'NTYz' +
'NDkyYW' +
'Q2ZjkxNzAwMDAxM' +
'DAwMDAxYzE2ODA' +
'0MGU2NjkzNGNlMT' +
'kzNjdmZjA5NGU2NDMyM2IK',
),
);

View File

@ -8,3 +8,12 @@ final UnmodifiableSetView<double> DEFAULT_ZOOM_LEVELS =
UnmodifiableSetView({0.6, 1, 2, 5, 10, 20, 50, 100});
const CALENDAR_DATE_IN_MAX_DELAY = Duration(milliseconds: 500);
const CACHE_INVALIDATION_DURATION = Duration(days: 7);
const WELCOME_SCREEN_PHOTOS_QUERIES = [
'happy',
'people',
'couple',
'family',
'fun',
'friends',
'romantic',
];

View File

@ -26,4 +26,10 @@ class StatusController extends PropertyChangeNotifier<String> {
_done = true;
notifyListeners('done');
}
void reset() {
_done = false;
_isForwarding = false;
notifyListeners('done');
}
}

View File

@ -3,6 +3,7 @@
"generalError": "Ein Fehler ist aufgetreten",
"generalCancelButtonLabel": "Abbrechen",
"generalContinueButtonLabel": "Weiter",
"welcomeScreenDescription": "Finde heraus was du den ganzen Tag gemacht hast und erlebe Erinnerungen wieder, die du komplett vergessen hast!",
"welcomeScreenSubtitle": "Was hab ich heute gemacht?",

View File

@ -3,10 +3,14 @@
"generalError": "There was an error",
"generalCancelButtonLabel": "Cancel",
"generalContinueButtonLabel": "Continue",
"welcomeScreenDescription": "Find out what you did all the days and unlock moments you completely forgot!",
"welcomeScreenSubtitle": "What did I do today?",
"welcomeScreenStartButtonTitle": "Start",
"welcomeScreenCreateMemoriesGuideDescription": "Create photos and video thorough the day...",
"welcomeScreenViewMemoriesGuideDescription": "...and relieve your best moments at the end of the day!",
"welcomeScreenGetStartedLabel": "Get started",
"serverLoadingScreenDescription": "We are loading your data",

View File

@ -0,0 +1,19 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
class PhotoManager {
static const MAX_PHOTOS_PER_PAGE = 80;
// Searches for photos based on `query` and returns a random one.
static Future<String> getRandomPhoto(final String query) async {
final url =
'https://api.pexels.com/v1/search?query=$query&per_page=$MAX_PHOTOS_PER_PAGE&orientation=portait';
final response = await http.get(Uri.parse(url));
final data = jsonDecode(response.body);
final photoIndex = Random().nextInt(data['per_page']);
return data['photos'][photoIndex]['src']['portrait'];
}
}

View File

@ -82,6 +82,7 @@ class _MemorySlideState extends State<MemorySlide>
return Consumer<TimelineModel>(
builder: (___, timeline, ____) => Status(
controller: controller,
isIndeterminate: controller == null,
paused: timeline.paused,
hideProgressBar: !timeline.showOverlay,
child: MemoryView(

View File

@ -2,71 +2,74 @@ 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/widgets/icon_button_child.dart';
import 'package:quid_faciam_hodie/widgets/logo.dart';
import 'grant_permission_screen.dart';
import 'welcome_screen/pages/get_started_page.dart';
import 'welcome_screen/pages/guide_page.dart';
import 'welcome_screen/pages/initial_page.dart';
class WelcomeScreen extends StatelessWidget {
class WelcomeScreen extends StatefulWidget {
static const ID = '/welcome';
const WelcomeScreen({Key? key}) : super(key: key);
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
final controller = PageController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
void nextPage() {
controller.animateToPage(
(controller.page! + 1).toInt(),
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return PlatformScaffold(
body: Padding(
padding: const EdgeInsets.all(MEDIUM_SPACE),
padding: const EdgeInsets.symmetric(vertical: MEDIUM_SPACE),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Logo(),
const SizedBox(height: LARGE_SPACE),
Text(
localizations.appTitleQuestion,
textAlign: TextAlign.center,
style: platformThemeData(
context,
material: (data) => data.textTheme.headline1,
cupertino: (data) => data.textTheme.navLargeTitleTextStyle,
),
),
const SizedBox(height: SMALL_SPACE),
Text(
localizations.welcomeScreenSubtitle,
style: platformThemeData(
context,
material: (data) => data.textTheme.bodySmall,
cupertino: (data) => data.textTheme.navTitleTextStyle,
),
),
const SizedBox(height: LARGE_SPACE),
Text(
localizations.welcomeScreenDescription,
textAlign: TextAlign.center,
style: platformThemeData(
context,
material: (data) => data.textTheme.bodyText1,
cupertino: (data) => data.textTheme.textStyle,
),
),
const SizedBox(height: LARGE_SPACE),
PlatformElevatedButton(
child: IconButtonChild(
icon: Icon(context.platformIcons.forward),
label: Text(localizations.welcomeScreenStartButtonTitle),
),
onPressed: () {
Navigator.pushNamed(
context,
GrantPermissionScreen.ID,
child: PageView.builder(
itemBuilder: (context, index) {
switch (index) {
case 0:
return InitialPage(
onNextPage: nextPage,
);
},
)
],
case 1:
return GuidePage(
onNextPage: nextPage,
description: localizations
.welcomeScreenCreateMemoriesGuideDescription,
picture: 'assets/images/live_photo.svg',
);
case 2:
return GuidePage(
onNextPage: nextPage,
description:
localizations.welcomeScreenViewMemoriesGuideDescription,
);
case 3:
return const GetStartedPage();
default:
return const SizedBox();
}
},
controller: controller,
itemCount: 4,
),
),
),

View File

@ -0,0 +1,15 @@
import 'package:coast/coast.dart';
import 'package:flutter/material.dart';
import 'package:quid_faciam_hodie/widgets/logo.dart';
class CrabLogo extends StatelessWidget {
const CrabLogo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Crab(
tag: 'logo',
child: Logo(),
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:coast/coast.dart';
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/widgets/icon_button_child.dart';
class CrabNextButton extends StatelessWidget {
final VoidCallback onPressed;
const CrabNextButton({
Key? key,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return Crab(
tag: 'next_button',
child: PlatformElevatedButton(
onPressed: onPressed,
child: IconButtonChild(
icon: Icon(context.platformIcons.forward),
label: Text(localizations.generalContinueButtonLabel),
),
),
);
}
}

View File

@ -0,0 +1,52 @@
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/screens/grant_permission_screen.dart';
import 'package:quid_faciam_hodie/screens/welcome_screen/crabs/logo.dart';
import 'package:quid_faciam_hodie/utils/theme.dart';
import 'package:quid_faciam_hodie/widgets/icon_button_child.dart';
class GetStartedPage extends StatelessWidget {
const GetStartedPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const CrabLogo(),
const SizedBox(height: LARGE_SPACE),
Text(
localizations.appTitleQuestion,
style: getTitleTextStyle(context),
),
const SizedBox(height: MEDIUM_SPACE),
Text(
localizations.welcomeScreenGetStartedLabel,
style: getSubTitleTextStyle(context),
),
const SizedBox(height: MEDIUM_SPACE),
PlatformElevatedButton(
child: IconButtonChild(
icon: Icon(context.platformIcons.forward),
label: Text(localizations.welcomeScreenStartButtonTitle),
),
onPressed: () {
Navigator.pushNamed(
context,
GrantPermissionScreen.ID,
);
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:quid_faciam_hodie/constants/spacing.dart';
import 'package:quid_faciam_hodie/screens/welcome_screen/crabs/next_button.dart';
import 'package:quid_faciam_hodie/screens/welcome_screen/photo_switching.dart';
import 'package:quid_faciam_hodie/utils/theme.dart';
class GuidePage extends StatelessWidget {
final String? picture;
final String description;
final VoidCallback onNextPage;
const GuidePage({
Key? key,
required this.description,
required this.onNextPage,
this.picture,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
picture == null
? const Expanded(
child: Padding(
padding: EdgeInsets.only(top: LARGE_SPACE),
child: PhotoSwitching(),
),
)
: SvgPicture.asset(picture!, height: 400),
const SizedBox(height: LARGE_SPACE),
Text(
description,
style: getBodyTextTextStyle(context),
),
const SizedBox(height: LARGE_SPACE),
CrabNextButton(onPressed: onNextPage),
],
),
),
);
}
}

View File

@ -0,0 +1,63 @@
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/screens/welcome_screen/crabs/logo.dart';
import 'package:quid_faciam_hodie/screens/welcome_screen/crabs/next_button.dart';
import 'package:quid_faciam_hodie/utils/theme.dart';
class InitialPage extends StatefulWidget {
final VoidCallback onNextPage;
const InitialPage({
Key? key,
required this.onNextPage,
}) : super(key: key);
@override
State<InitialPage> createState() => _InitialPageState();
}
class _InitialPageState extends State<InitialPage> {
late String photoURL;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const CrabLogo(),
const SizedBox(height: HUGE_SPACE),
Text(
localizations.appTitleQuestion,
style: getTitleTextStyle(context),
),
const SizedBox(height: SMALL_SPACE),
Text(
localizations.welcomeScreenSubtitle,
style: getSubTitleTextStyle(context),
),
const SizedBox(height: MEDIUM_SPACE),
Text(
localizations.welcomeScreenDescription,
style: getBodyTextTextStyle(context),
),
const SizedBox(height: LARGE_SPACE),
CrabNextButton(
onPressed: widget.onNextPage,
)
],
),
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:quid_faciam_hodie/constants/spacing.dart';
import 'package:quid_faciam_hodie/constants/values.dart';
import 'package:quid_faciam_hodie/managers/photo_manager.dart';
import 'package:quid_faciam_hodie/utils/loadable.dart';
import 'package:quid_faciam_hodie/widgets/status.dart';
class PhotoSwitching extends StatefulWidget {
const PhotoSwitching({Key? key}) : super(key: key);
@override
State<PhotoSwitching> createState() => _PhotoSwitchingState();
}
class _PhotoSwitchingState extends State<PhotoSwitching> with Loadable {
late String photoURL;
@override
void initState() {
super.initState();
callWithLoading(getNextPhoto);
}
Future<void> getNextPhoto() async {
final query = WELCOME_SCREEN_PHOTOS_QUERIES[
Random().nextInt(WELCOME_SCREEN_PHOTOS_QUERIES.length)];
final url = await PhotoManager.getRandomPhoto(query);
if (!mounted) {
return;
}
setState(() {
photoURL = url;
});
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return ClipRRect(
borderRadius: BorderRadius.circular(MEDIUM_SPACE),
child: Image.network(
photoURL,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return Status(
autoStart: true,
onEnd: () async {
getNextPhoto();
},
duration: const Duration(seconds: 3),
child: child,
);
}
return const SizedBox.expand();
},
fit: BoxFit.cover,
),
);
}
}

View File

@ -9,12 +9,20 @@ class Status extends StatefulWidget {
final Widget child;
final bool paused;
final bool hideProgressBar;
final bool autoStart;
final bool isIndeterminate;
final VoidCallback? onEnd;
final Duration duration;
const Status({
Key? key,
required this.child,
this.paused = false,
this.hideProgressBar = false,
this.autoStart = false,
this.isIndeterminate = false,
this.duration = const Duration(seconds: 5),
this.onEnd,
this.controller,
}) : super(key: key);
@ -23,7 +31,7 @@ class Status extends StatefulWidget {
}
class _StatusState extends State<Status> with TickerProviderStateMixin {
late final Animation<double> animation;
Animation<double>? animation;
AnimationController? animationController;
@override
@ -48,28 +56,48 @@ class _StatusState extends State<Status> with TickerProviderStateMixin {
super.dispose();
}
@override
void initState() {
super.initState();
if (widget.autoStart) {
initializeAnimation();
}
}
void initializeAnimation() {
animationController = AnimationController(
duration: widget.controller!.duration,
duration: widget.controller?.duration ?? widget.duration,
vsync: this,
);
)..addStatusListener((status) {
if (status == AnimationStatus.completed) {
widget.onEnd?.call();
}
});
animation =
Tween<double>(begin: 0.0, end: 1.0).animate(animationController!)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
widget.controller!.setDone();
widget.controller?.setDone();
}
});
animationController!.forward();
widget.controller!.addListener(() {
if (widget.controller!.isForwarding) {
animationController!.forward();
} else {
animationController!.stop();
}
});
if (widget.controller != null) {
widget.controller!.addListener(() {
if (widget.controller!.isForwarding) {
animationController!.forward();
} else {
animationController!.stop();
}
});
}
if (widget.autoStart) {
animationController?.forward();
}
}
@override
@ -82,8 +110,8 @@ class _StatusState extends State<Status> with TickerProviderStateMixin {
widget.child,
Positioned(
left: 0,
right: 0,
bottom: SMALL_SPACE,
width: MediaQuery.of(context).size.width,
height: BAR_HEIGHT,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE),
@ -91,7 +119,7 @@ class _StatusState extends State<Status> with TickerProviderStateMixin {
duration: const Duration(milliseconds: 500),
curve: Curves.linearToEaseOut,
opacity: widget.hideProgressBar ? 0.0 : 1.0,
child: (widget.controller == null)
child: (widget.isIndeterminate || animation == null)
? ClipRRect(
borderRadius: BorderRadius.circular(HUGE_SPACE),
child: LinearProgressIndicator(
@ -102,13 +130,13 @@ class _StatusState extends State<Status> with TickerProviderStateMixin {
),
)
: AnimatedBuilder(
animation: animation,
animation: animation!,
builder: (_, __) => ClipRRect(
borderRadius: BorderRadius.circular(HUGE_SPACE),
child: LinearProgressIndicator(
value: animation.value,
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.white),
value: animation!.value,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white),
backgroundColor: Colors.white.withOpacity(0.1),
),
),

View File

@ -78,6 +78,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
coast:
dependency: "direct main"
description:
name: coast
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
collection:
dependency: transitive
description:
@ -295,7 +302,7 @@ packages:
source: hosted
version: "0.15.0"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"

View File

@ -56,6 +56,8 @@ dependencies:
flutter_platform_widgets: ^2.0.0
lottie: ^1.4.1
settings_ui: ^2.0.2
coast: ^2.0.2
http: ^0.13.5
dev_dependencies:
flutter_test:
@ -83,6 +85,7 @@ flutter:
assets:
- assets/
- assets/lottie/flying-astronaut.json
- assets/images/
# To add assets to your application, add an assets section, like this:
# assets: