2022-08-29 20:38:28 -06:00
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
|
|
import 'package:wonders/common_libs.dart';
|
2023-12-04 11:30:11 -07:00
|
|
|
import 'package:wonders/logic/common/platform_info.dart';
|
2023-12-08 09:54:26 -07:00
|
|
|
import 'package:wonders/ui/common/app_icons.dart';
|
2022-08-29 20:38:28 -06:00
|
|
|
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
2022-12-01 20:09:28 -07:00
|
|
|
import 'package:wonders/ui/common/gradient_container.dart';
|
2023-12-04 11:30:11 -07:00
|
|
|
import 'package:wonders/ui/common/previous_next_navigation.dart';
|
2022-09-01 20:46:15 -06:00
|
|
|
import 'package:wonders/ui/common/static_text_scale.dart';
|
2022-08-29 20:38:28 -06:00
|
|
|
import 'package:wonders/ui/common/themed_text.dart';
|
|
|
|
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
|
|
|
|
|
|
|
class IntroScreen extends StatefulWidget {
|
2024-02-20 13:56:39 -08:00
|
|
|
const IntroScreen({super.key});
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
@override
|
|
|
|
State<IntroScreen> createState() => _IntroScreenState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _IntroScreenState extends State<IntroScreen> {
|
2023-01-23 09:46:01 -07:00
|
|
|
static const double _imageSize = 250;
|
2022-08-29 20:38:28 -06:00
|
|
|
static const double _logoHeight = 126;
|
2023-09-05 09:45:59 -06:00
|
|
|
static const double _textHeight = 110;
|
2022-08-29 20:38:28 -06:00
|
|
|
static const double _pageIndicatorHeight = 55;
|
|
|
|
|
2022-09-01 14:45:55 -06:00
|
|
|
static List<_PageData> pageData = [];
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
late final PageController _pageController = PageController()..addListener(_handlePageChanged);
|
2023-12-04 11:30:11 -07:00
|
|
|
late final ValueNotifier<int> _currentPage = ValueNotifier(0)..addListener(() => setState(() {}));
|
2023-01-03 10:35:26 -07:00
|
|
|
bool get _isOnLastPage => _currentPage.value.round() == pageData.length - 1;
|
2023-07-31 10:14:04 -06:00
|
|
|
bool get _isOnFirstPage => _currentPage.value.round() == 0;
|
|
|
|
|
2022-08-29 20:38:28 -06:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_pageController.dispose();
|
2023-12-04 11:30:11 -07:00
|
|
|
_currentPage.dispose();
|
2022-08-29 20:38:28 -06:00
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _handleIntroCompletePressed() {
|
2022-09-10 14:15:24 +05:30
|
|
|
if (_currentPage.value == pageData.length - 1) {
|
|
|
|
context.go(ScreenPaths.home);
|
|
|
|
settingsLogic.hasCompletedOnboarding.value = true;
|
|
|
|
}
|
2022-08-29 20:38:28 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
void _handlePageChanged() {
|
|
|
|
int newPage = _pageController.page?.round() ?? 0;
|
|
|
|
_currentPage.value = newPage;
|
|
|
|
}
|
|
|
|
|
|
|
|
void _handleSemanticSwipe(int dir) {
|
|
|
|
_pageController.animateToPage((_pageController.page ?? 0).round() + dir,
|
|
|
|
duration: $styles.times.fast, curve: Curves.easeOut);
|
|
|
|
}
|
|
|
|
|
2023-07-31 10:14:04 -06:00
|
|
|
void _handleNavTextSemanticTap() => _incrementPage(1);
|
|
|
|
|
2023-12-04 11:30:11 -07:00
|
|
|
void _incrementPage(int dir) {
|
2023-01-01 13:32:10 -07:00
|
|
|
final int current = _pageController.page!.round();
|
2023-07-31 10:14:04 -06:00
|
|
|
if (_isOnLastPage && dir > 0) return;
|
|
|
|
if (_isOnFirstPage && dir < 0) return;
|
|
|
|
_pageController.animateToPage(current + dir, duration: 250.ms, curve: Curves.easeIn);
|
2023-01-01 13:32:10 -07:00
|
|
|
}
|
|
|
|
|
2022-08-29 20:38:28 -06:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-09-01 14:45:55 -06:00
|
|
|
// Set the page data, as strings may have changed based on locale
|
|
|
|
pageData = [
|
|
|
|
_PageData($strings.introTitleJourney, $strings.introDescriptionNavigate, 'camel', '1'),
|
|
|
|
_PageData($strings.introTitleExplore, $strings.introDescriptionUncover, 'petra', '2'),
|
|
|
|
_PageData($strings.introTitleDiscover, $strings.introDescriptionLearn, 'statue', '3'),
|
|
|
|
];
|
|
|
|
|
2022-08-29 20:38:28 -06:00
|
|
|
// This view uses a full screen PageView to enable swipe navigation.
|
|
|
|
// However, we only want the title / description to actually swipe,
|
|
|
|
// so we stack a PageView with that content over top of all the other
|
|
|
|
// content, and line up their layouts.
|
|
|
|
final List<Widget> pages = pageData.map((e) => _Page(data: e)).toList();
|
|
|
|
|
2023-01-03 10:35:26 -07:00
|
|
|
/// Return resulting widget tree
|
2023-12-04 11:30:11 -07:00
|
|
|
return DefaultTextColor(
|
|
|
|
color: $styles.colors.offWhite,
|
|
|
|
child: ColoredBox(
|
|
|
|
color: $styles.colors.black,
|
|
|
|
child: SafeArea(
|
|
|
|
child: Animate(
|
|
|
|
delay: 500.ms,
|
|
|
|
effects: const [FadeEffect()],
|
|
|
|
child: PreviousNextNavigation(
|
|
|
|
maxWidth: 600,
|
|
|
|
nextBtnColor: _isOnLastPage ? $styles.colors.accent1 : null,
|
|
|
|
onPreviousPressed: _isOnFirstPage ? null : () => _incrementPage(-1),
|
|
|
|
onNextPressed: () {
|
|
|
|
if (_isOnLastPage) {
|
|
|
|
_handleIntroCompletePressed();
|
|
|
|
} else {
|
|
|
|
_incrementPage(1);
|
|
|
|
}
|
|
|
|
},
|
2023-07-31 10:14:04 -06:00
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
// page view with title & description:
|
|
|
|
MergeSemantics(
|
|
|
|
child: Semantics(
|
|
|
|
onIncrease: () => _handleSemanticSwipe(1),
|
|
|
|
onDecrease: () => _handleSemanticSwipe(-1),
|
|
|
|
child: PageView(
|
|
|
|
controller: _pageController,
|
|
|
|
children: pages,
|
|
|
|
onPageChanged: (_) => AppHaptics.lightImpact(),
|
|
|
|
),
|
2023-01-03 10:35:26 -07:00
|
|
|
),
|
|
|
|
),
|
2023-07-31 10:14:04 -06:00
|
|
|
|
|
|
|
IgnorePointer(
|
|
|
|
ignoringSemantics: false,
|
|
|
|
child: Column(children: [
|
|
|
|
Spacer(),
|
|
|
|
|
|
|
|
// logo:
|
|
|
|
Semantics(
|
|
|
|
header: true,
|
|
|
|
child: Container(
|
|
|
|
height: _logoHeight,
|
|
|
|
alignment: Alignment.center,
|
|
|
|
child: _WonderousLogo(),
|
|
|
|
),
|
2023-01-03 10:35:26 -07:00
|
|
|
),
|
|
|
|
|
2023-07-31 10:14:04 -06:00
|
|
|
// masked image:
|
|
|
|
SizedBox(
|
|
|
|
height: _imageSize,
|
|
|
|
width: _imageSize,
|
|
|
|
child: ValueListenableBuilder<int>(
|
|
|
|
valueListenable: _currentPage,
|
|
|
|
builder: (_, value, __) {
|
|
|
|
return AnimatedSwitcher(
|
|
|
|
duration: $styles.times.slow,
|
|
|
|
child: KeyedSubtree(
|
|
|
|
key: ValueKey(value), // so AnimatedSwitcher sees it as a different child.
|
|
|
|
child: _PageImage(data: pageData[value]),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
2023-01-03 10:35:26 -07:00
|
|
|
),
|
|
|
|
|
2023-07-31 10:14:04 -06:00
|
|
|
// placeholder gap for text:
|
|
|
|
Gap(_IntroScreenState._textHeight),
|
2023-01-03 10:35:26 -07:00
|
|
|
|
2023-07-31 10:14:04 -06:00
|
|
|
// page indicator:
|
|
|
|
Container(
|
|
|
|
height: _pageIndicatorHeight,
|
|
|
|
alignment: Alignment(0.0, 0),
|
|
|
|
child: AppPageIndicator(
|
|
|
|
count: pageData.length, controller: _pageController, color: $styles.colors.offWhite),
|
|
|
|
),
|
|
|
|
|
|
|
|
Spacer(flex: 2),
|
|
|
|
]),
|
|
|
|
),
|
2023-01-03 10:35:26 -07:00
|
|
|
|
2023-07-31 10:14:04 -06:00
|
|
|
// Build a cpl overlays to hide the content when swiping on very wide screens
|
|
|
|
_buildHzGradientOverlay(left: true),
|
|
|
|
_buildHzGradientOverlay(),
|
|
|
|
|
|
|
|
// nav help text:
|
2023-12-04 11:30:11 -07:00
|
|
|
if (PlatformInfo.isMobile) ...[
|
2023-12-08 09:54:26 -07:00
|
|
|
// finish button:
|
|
|
|
Positioned(
|
|
|
|
right: $styles.insets.lg,
|
|
|
|
bottom: $styles.insets.lg,
|
|
|
|
child: _buildFinishBtn(context),
|
|
|
|
),
|
|
|
|
|
2023-12-04 11:30:11 -07:00
|
|
|
BottomCenter(
|
|
|
|
child: Padding(
|
|
|
|
padding: EdgeInsets.only(bottom: $styles.insets.lg),
|
|
|
|
child: _buildNavText(context),
|
|
|
|
),
|
2023-07-31 10:14:04 -06:00
|
|
|
),
|
2023-12-04 11:30:11 -07:00
|
|
|
]
|
2023-07-31 10:14:04 -06:00
|
|
|
],
|
|
|
|
),
|
2023-01-03 10:35:26 -07:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2022-08-29 20:38:28 -06:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-08 09:54:26 -07:00
|
|
|
Widget _buildFinishBtn(BuildContext context) {
|
|
|
|
return ValueListenableBuilder<int>(
|
|
|
|
valueListenable: _currentPage,
|
|
|
|
builder: (_, pageIndex, __) {
|
|
|
|
return AnimatedOpacity(
|
|
|
|
opacity: pageIndex == pageData.length - 1 ? 1 : 0,
|
|
|
|
duration: $styles.times.fast,
|
|
|
|
child: CircleIconBtn(
|
|
|
|
icon: AppIcons.next_large,
|
|
|
|
bgColor: $styles.colors.accent1,
|
|
|
|
onPressed: _handleIntroCompletePressed,
|
|
|
|
semanticLabel: $strings.introSemanticEnterApp,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-01 20:09:28 -07:00
|
|
|
Widget _buildHzGradientOverlay({bool left = false}) {
|
|
|
|
return Align(
|
|
|
|
alignment: Alignment(left ? -1 : 1, 0),
|
|
|
|
child: FractionallySizedBox(
|
|
|
|
widthFactor: .5,
|
|
|
|
child: Padding(
|
|
|
|
padding: EdgeInsets.only(left: left ? 0 : 200, right: left ? 200 : 0),
|
|
|
|
child: Transform.scale(
|
|
|
|
scaleX: left ? -1 : 1,
|
|
|
|
child: HzGradient([
|
|
|
|
$styles.colors.black.withOpacity(0),
|
|
|
|
$styles.colors.black,
|
|
|
|
], const [
|
|
|
|
0,
|
|
|
|
.2
|
|
|
|
])),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-29 20:38:28 -06:00
|
|
|
Widget _buildNavText(BuildContext context) {
|
|
|
|
return ValueListenableBuilder(
|
|
|
|
valueListenable: _currentPage,
|
|
|
|
builder: (_, pageIndex, __) {
|
|
|
|
return AnimatedOpacity(
|
|
|
|
opacity: pageIndex == pageData.length - 1 ? 0 : 1,
|
|
|
|
duration: $styles.times.fast,
|
|
|
|
child: Semantics(
|
|
|
|
onTapHint: $strings.introSemanticNavigate,
|
2023-07-31 10:14:04 -06:00
|
|
|
onTap: _isOnLastPage ? null : _handleNavTextSemanticTap,
|
2022-08-29 20:38:28 -06:00
|
|
|
child: Text($strings.introSemanticSwipeLeft, style: $styles.text.bodySmall),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@immutable
|
|
|
|
class _PageData {
|
|
|
|
const _PageData(this.title, this.desc, this.img, this.mask);
|
|
|
|
|
|
|
|
final String title;
|
|
|
|
final String desc;
|
|
|
|
final String img;
|
|
|
|
final String mask;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _Page extends StatelessWidget {
|
2024-02-20 13:56:39 -08:00
|
|
|
const _Page({required this.data});
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
final _PageData data;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Semantics(
|
|
|
|
liveRegion: true,
|
2023-01-03 22:30:27 -07:00
|
|
|
child: Padding(
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: $styles.insets.md),
|
|
|
|
child: Column(children: [
|
|
|
|
Spacer(),
|
|
|
|
Gap(_IntroScreenState._imageSize + _IntroScreenState._logoHeight),
|
|
|
|
SizedBox(
|
|
|
|
height: _IntroScreenState._textHeight,
|
|
|
|
width: 400,
|
|
|
|
child: StaticTextScale(
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Text(data.title, style: $styles.text.wonderTitle.copyWith(fontSize: 24 * $styles.scale)),
|
|
|
|
Gap($styles.insets.sm),
|
|
|
|
Text(data.desc, style: $styles.text.body, textAlign: TextAlign.center),
|
|
|
|
],
|
|
|
|
),
|
2022-09-01 20:46:15 -06:00
|
|
|
),
|
2022-08-29 20:38:28 -06:00
|
|
|
),
|
2023-01-03 22:30:27 -07:00
|
|
|
Gap(_IntroScreenState._pageIndicatorHeight),
|
|
|
|
Spacer(flex: 2),
|
|
|
|
]),
|
|
|
|
),
|
2022-08-29 20:38:28 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _WonderousLogo extends StatelessWidget {
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
ExcludeSemantics(
|
2023-04-18 11:12:03 -06:00
|
|
|
child: SvgPicture.asset(SvgPaths.compassSimple, colorFilter: $styles.colors.offWhite.colorFilter, height: 48),
|
2022-08-29 20:38:28 -06:00
|
|
|
),
|
|
|
|
Gap($styles.insets.xs),
|
2022-09-01 20:46:15 -06:00
|
|
|
StaticTextScale(
|
|
|
|
child: Text(
|
|
|
|
$strings.introSemanticWonderous,
|
2022-10-24 15:42:27 -06:00
|
|
|
style: $styles.text.wonderTitle.copyWith(fontSize: 32 * $styles.scale, color: $styles.colors.offWhite),
|
2022-09-01 20:46:15 -06:00
|
|
|
),
|
2022-08-29 20:38:28 -06:00
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _PageImage extends StatelessWidget {
|
2024-02-20 13:56:39 -08:00
|
|
|
const _PageImage({required this.data});
|
2022-08-29 20:38:28 -06:00
|
|
|
|
|
|
|
final _PageData data;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Stack(
|
|
|
|
children: [
|
|
|
|
SizedBox.expand(
|
|
|
|
child: Image.asset(
|
|
|
|
'${ImagePaths.common}/intro-${data.img}.jpg',
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
alignment: Alignment.centerRight,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned.fill(
|
|
|
|
child: Image.asset(
|
|
|
|
'${ImagePaths.common}/intro-mask-${data.mask}.png',
|
|
|
|
fit: BoxFit.fill,
|
|
|
|
)),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|