Files
immich/mobile/lib/pages/onboarding/onboarding.page.dart
T
2024-12-20 10:12:52 -06:00

327 lines
9.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@RoutePage()
class OnboardingPage extends HookConsumerWidget {
const OnboardingPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final pageController = usePageController(keepPage: false);
toNextPage() {
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
return Scaffold(
appBar: AppBar(
title: SvgPicture.asset(
context.isDarkTheme
? 'assets/immich-logo-inline-dark.svg'
: 'assets/immich-logo-inline-light.svg',
height: 48,
),
centerTitle: false,
elevation: 0,
),
body: SafeArea(
child: PageView(
controller: pageController,
// physics: const NeverScrollableScrollPhysics(),
children: [
OnboardingWelcome(
onNextPage: () => toNextPage(),
),
OnboardingGalleryPermission(
onNextPage: () => toNextPage(),
),
],
),
),
);
}
}
class OnboardingWelcome extends StatelessWidget {
final VoidCallback onNextPage;
const OnboardingWelcome({super.key, required this.onNextPage});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(18.0),
child: ListView(
physics: const ClampingScrollPhysics(),
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Card(
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(32),
),
),
elevation: 3,
child: AnimatedHeroImage(
imagePath: 'assets/onboarding-1-screenshot.jpeg',
color: context.colorScheme.primary.withOpacity(0.25),
colorBlendMode: BlendMode.color,
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 32.0,
left: 8.0,
bottom: 8.0,
),
child: Text(
"WELCOME",
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurface.withOpacity(0.6),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
"Lets get you setup with some permissions that the app needs",
style: context.textTheme.headlineSmall,
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(right: 8.0, top: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: 64,
height: 64,
child: MaterialButton(
onPressed: onNextPage,
color: context.primaryColor,
textColor: Colors.white,
shape: const CircleBorder(),
child: Icon(
Icons.chevron_right_rounded,
color: context.colorScheme.onPrimary,
size: 32,
),
),
),
],
),
),
],
),
);
}
}
class AnimatedHeroImage extends StatefulWidget {
final String imagePath;
final Color color;
final BlendMode colorBlendMode;
const AnimatedHeroImage({
super.key,
required this.imagePath,
required this.color,
required this.colorBlendMode,
});
@override
AnimatedHeroImageState createState() => AnimatedHeroImageState();
}
class AnimatedHeroImageState extends State<AnimatedHeroImage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
late Animation<Offset> _parallaxAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 15),
vsync: this,
)..repeat(reverse: true);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
_rotationAnimation = Tween<double>(begin: 0.0, end: 0.025).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
_parallaxAnimation =
Tween<Offset>(begin: Offset.zero, end: const Offset(0.05, 0.05))
.animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Image(
image: AssetImage(widget.imagePath),
filterQuality: FilterQuality.high,
isAntiAlias: true,
// color: widget.color,
// colorBlendMode: widget.colorBlendMode,
),
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.translate(
offset: _parallaxAnimation.value,
child: child,
),
),
);
},
);
}
}
class OnboardingGalleryPermission extends StatelessWidget {
final VoidCallback onNextPage;
const OnboardingGalleryPermission({super.key, required this.onNextPage});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
Icons.perm_media_outlined,
size: 24,
color: context.primaryColor.withAlpha(250),
),
const SizedBox(width: 16),
Text(
"Gallery Permission",
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
),
],
),
const SizedBox(height: 16),
Text(
"We use the read and write permission of the media gallery for the following actions",
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w400,
color: context.colorScheme.onSurface.withAlpha(220),
),
),
const SizedBox(height: 40),
const BulletList([
'Display the local videos and images',
'Read the file content to upload to your Immich instance',
'Remove media from the device on your request',
]),
const Spacer(),
SizedBox(
height: 48,
width: double.infinity,
child: ElevatedButton(
onPressed: onNextPage,
child: const Text(
'OK',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
}
class BulletList extends StatelessWidget {
final List<String> strings;
const BulletList(this.strings, {super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: strings.map((textString) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'\u2022',
style: TextStyle(
fontSize: 20,
height: 1.25,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
textString,
textAlign: TextAlign.left,
softWrap: true,
style: context.textTheme.headlineSmall?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}).toList(),
),
);
}
}