diff --git a/assets/images/shaynee_avatar.png b/assets/images/shaynee_avatar.png new file mode 100644 index 0000000..c76e290 Binary files /dev/null and b/assets/images/shaynee_avatar.png differ diff --git a/assets/images/shaynee_hero.png b/assets/images/shaynee_hero.png new file mode 100644 index 0000000..022b02f Binary files /dev/null and b/assets/images/shaynee_hero.png differ diff --git a/lib/main.dart b/lib/main.dart index 244a702..b924dd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,122 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'theme.dart'; +import 'screens/splash_screen.dart'; void main() { - runApp(const MyApp()); + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle(statusBarBrightness: Brightness.dark), + ); + runApp(const ShayneeApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class ShayneeApp extends StatelessWidget { + const ShayneeApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: .fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: .center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), + title: 'Shaynee', + debugShowCheckedModeBanner: false, + theme: buildTheme(), + home: const SplashScreen(), ); } } diff --git a/lib/screens/intro_screen.dart b/lib/screens/intro_screen.dart new file mode 100644 index 0000000..bda7714 --- /dev/null +++ b/lib/screens/intro_screen.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../theme.dart'; +import '../widgets/primary_button.dart'; +import 'quiz/quiz_flow.dart'; + +class IntroScreen extends StatelessWidget { + const IntroScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SColors.bgPage, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + _badge() + .animate() + .fadeIn(duration: 400.ms), + const SizedBox(height: 20), + Text( + 'Skin that glows,\nroutine that works.', + style: GoogleFonts.plusJakartaSans( + fontSize: 32, + fontWeight: FontWeight.w800, + color: SColors.textPrimary, + height: 1.2, + ), + ) + .animate() + .fadeIn(duration: 500.ms, delay: 100.ms) + .slideY(begin: 0.1, end: 0, duration: 500.ms), + const SizedBox(height: 12), + Text( + 'Answer 5 quick questions and get your\npersonalised skincare plan — in 60 seconds.', + style: GoogleFonts.plusJakartaSans( + fontSize: 15, + color: SColors.textSecondary, + height: 1.5, + ), + ) + .animate() + .fadeIn(duration: 500.ms, delay: 200.ms), + const SizedBox(height: 32), + _statsRow() + .animate() + .fadeIn(duration: 500.ms, delay: 300.ms), + const SizedBox(height: 32), + _featureList() + .animate() + .fadeIn(duration: 500.ms, delay: 400.ms), + const Spacer(), + PrimaryButton( + label: 'Build my routine', + icon: Icons.arrow_forward_rounded, + onTap: () => Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (_, a, __) => const QuizFlow(), + transitionsBuilder: (_, a, __, child) => + SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: a, curve: Curves.easeOut)), + child: child, + ), + transitionDuration: const Duration(milliseconds: 350), + ), + ), + ) + .animate() + .fadeIn(duration: 400.ms, delay: 600.ms) + .slideY(begin: 0.2, end: 0, duration: 400.ms), + const SizedBox(height: 12), + Center( + child: Text( + 'Free · No credit card · 1 min setup', + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + color: SColors.textSecondary, + ), + ), + ) + .animate() + .fadeIn(duration: 400.ms, delay: 700.ms), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _badge() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: SColors.primary.withAlpha(18), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: SColors.accent, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + 'Trusted by 50,000+ women', + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + fontWeight: FontWeight.w600, + color: SColors.primary, + ), + ), + ], + ), + ); + } + + Widget _statsRow() { + return Row( + children: [ + _statCard('50K+', 'Users'), + const SizedBox(width: 12), + _statCard('4.9★', 'Rating'), + const SizedBox(width: 12), + _statCard('94%', 'See results'), + ], + ); + } + + Widget _statCard(String value, String label) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: SColors.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: SColors.border), + ), + child: Column( + children: [ + Text( + value, + style: GoogleFonts.plusJakartaSans( + fontSize: 20, + fontWeight: FontWeight.w800, + color: SColors.primary, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + color: SColors.textSecondary, + ), + ), + ], + ), + ), + ); + } + + Widget _featureList() { + const items = [ + (Icons.auto_awesome_rounded, 'AI-powered analysis', 'Personalised to your skin'), + (Icons.science_rounded, 'Ingredient science', 'Evidence-based recommendations'), + (Icons.lock_outline_rounded, 'Private & secure', 'Your data stays with you'), + ]; + return Column( + children: items.map((item) => _featureRow(item.$1, item.$2, item.$3)).toList(), + ); + } + + Widget _featureRow(IconData icon, String title, String sub) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: SColors.primary.withAlpha(18), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: SColors.primary, size: 20), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: GoogleFonts.plusJakartaSans( + fontSize: 14, + fontWeight: FontWeight.w600, + color: SColors.textPrimary, + ), + ), + Text( + sub, + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + color: SColors.textSecondary, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/screens/quiz/quiz_flow.dart b/lib/screens/quiz/quiz_flow.dart new file mode 100644 index 0000000..025ca8e --- /dev/null +++ b/lib/screens/quiz/quiz_flow.dart @@ -0,0 +1,500 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../theme.dart'; +import '../../widgets/primary_button.dart'; +import '../../widgets/quiz_option_row.dart'; +import '../result_screen.dart'; + +class QuizData { + String name = ''; + String skinType = ''; + String age = ''; + List concerns = []; + String goal = ''; +} + +class QuizFlow extends StatefulWidget { + const QuizFlow({super.key}); + + @override + State createState() => _QuizFlowState(); +} + +class _QuizFlowState extends State { + final _data = QuizData(); + int _step = 0; + final _pageCtrl = PageController(); + final _nameCtrl = TextEditingController(); + + static const _totalSteps = 5; + + void _next() { + if (_step < _totalSteps - 1) { + setState(() => _step++); + _pageCtrl.animateToPage( + _step, + duration: const Duration(milliseconds: 350), + curve: Curves.easeOut, + ); + } else { + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (_, a, __) => ResultScreen(data: _data), + transitionsBuilder: (_, a, __, child) => + FadeTransition(opacity: a, child: child), + transitionDuration: const Duration(milliseconds: 500), + ), + ); + } + } + + void _back() { + if (_step > 0) { + setState(() => _step--); + _pageCtrl.animateToPage( + _step, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } else { + Navigator.of(context).pop(); + } + } + + bool get _canProceed { + return switch (_step) { + 0 => _data.name.trim().length >= 2, + 1 => _data.skinType.isNotEmpty, + 2 => _data.age.isNotEmpty, + 3 => _data.concerns.isNotEmpty, + 4 => _data.goal.isNotEmpty, + _ => false, + }; + } + + @override + void dispose() { + _nameCtrl.dispose(); + _pageCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SColors.bgPage, + body: SafeArea( + child: Column( + children: [ + _topBar(), + _progressBar(), + Expanded( + child: PageView( + controller: _pageCtrl, + physics: const NeverScrollableScrollPhysics(), + children: [ + _NameStep( + ctrl: _nameCtrl, + onChanged: (v) => setState(() => _data.name = v), + ), + _SkinTypeStep( + selected: _data.skinType, + onSelect: (v) => setState(() => _data.skinType = v), + ), + _AgeStep( + selected: _data.age, + onSelect: (v) => setState(() => _data.age = v), + ), + _ConcernsStep( + selected: _data.concerns, + onToggle: (v) => setState(() { + if (_data.concerns.contains(v)) { + _data.concerns.remove(v); + } else if (_data.concerns.length < 3) { + _data.concerns.add(v); + } + }), + ), + _GoalStep( + selected: _data.goal, + onSelect: (v) => setState(() => _data.goal = v), + ), + ], + ), + ), + _bottomBar(), + ], + ), + ), + ); + } + + Widget _topBar() { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Row( + children: [ + GestureDetector( + onTap: _back, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: SColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: SColors.border), + ), + child: const Icon(Icons.arrow_back_ios_new_rounded, size: 16, color: SColors.textPrimary), + ), + ), + const Spacer(), + Text( + '${_step + 1} of $_totalSteps', + style: GoogleFonts.plusJakartaSans( + fontSize: 13, + fontWeight: FontWeight.w600, + color: SColors.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _progressBar() { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: (_step + 1) / _totalSteps, + backgroundColor: SColors.border, + valueColor: const AlwaysStoppedAnimation(SColors.primary), + minHeight: 4, + ), + ), + ); + } + + Widget _bottomBar() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 28), + child: PrimaryButton( + label: _step == _totalSteps - 1 ? 'See my results ✨' : 'Continue', + icon: _step == _totalSteps - 1 ? null : Icons.arrow_forward_rounded, + enabled: _canProceed, + onTap: _next, + ), + ); + } +} + +// ── Step 1: Name ────────────────────────────────────────────────────────────── + +class _NameStep extends StatelessWidget { + final TextEditingController ctrl; + final ValueChanged onChanged; + + const _NameStep({required this.ctrl, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hey! What\'s\nyour name? 👋', + style: GoogleFonts.plusJakartaSans( + fontSize: 30, + fontWeight: FontWeight.w800, + color: SColors.textPrimary, + height: 1.2, + ), + ).animate().fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0), + const SizedBox(height: 8), + Text( + 'I\'ll personalise your routine just for you.', + style: GoogleFonts.plusJakartaSans( + fontSize: 15, + color: SColors.textSecondary, + ), + ).animate().fadeIn(duration: 400.ms, delay: 100.ms), + const SizedBox(height: 36), + TextField( + controller: ctrl, + onChanged: onChanged, + autofocus: true, + textCapitalization: TextCapitalization.words, + style: GoogleFonts.plusJakartaSans( + fontSize: 18, + fontWeight: FontWeight.w600, + color: SColors.textPrimary, + ), + decoration: InputDecoration( + hintText: 'Your first name', + hintStyle: GoogleFonts.plusJakartaSans( + color: SColors.textSecondary, + fontWeight: FontWeight.w400, + ), + filled: true, + fillColor: SColors.surface, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: SColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: SColors.border, width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: SColors.primary, width: 2), + ), + ), + ).animate().fadeIn(duration: 400.ms, delay: 200.ms), + ], + ), + ); + } +} + +// ── Step 2: Skin Type ───────────────────────────────────────────────────────── + +class _SkinTypeStep extends StatelessWidget { + final String selected; + final ValueChanged onSelect; + + const _SkinTypeStep({required this.selected, required this.onSelect}); + + static const _options = [ + (Icons.water_drop_rounded, 'Oily', 'Shiny by midday, large pores'), + (Icons.grass_rounded, 'Dry', 'Tight, flaky, rough patches'), + (Icons.blur_on_rounded, 'Combination', 'Oily T-zone, dry cheeks'), + (Icons.sentiment_satisfied_alt_rounded, 'Normal', 'Balanced, minimal issues'), + (Icons.help_outline_rounded, 'Not sure', 'Help me figure it out'), + ]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'What\'s your\nskin type?', + style: GoogleFonts.plusJakartaSans( + fontSize: 30, + fontWeight: FontWeight.w800, + color: SColors.textPrimary, + height: 1.2, + ), + ).animate().fadeIn(duration: 400.ms), + const SizedBox(height: 8), + Text( + 'Choose the one that best describes your skin.', + style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary), + ).animate().fadeIn(duration: 400.ms, delay: 100.ms), + const SizedBox(height: 24), + ...List.generate(_options.length, (i) { + final (icon, label, sub) = _options[i]; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: QuizOptionRow( + icon: icon, + label: label, + sub: sub, + isSelected: selected == label, + onTap: () => onSelect(label), + ).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms) + .slideX(begin: 0.05, end: 0, duration: 300.ms), + ); + }), + ], + ), + ); + } +} + +// ── Step 3: Age ─────────────────────────────────────────────────────────────── + +class _AgeStep extends StatelessWidget { + final String selected; + final ValueChanged onSelect; + + const _AgeStep({required this.selected, required this.onSelect}); + + static const _options = [ + (Icons.star_rounded, 'Under 25', 'Young & preventive care'), + (Icons.hourglass_bottom_rounded, '25–35', 'Early anti-aging focus'), + (Icons.wb_sunny_rounded, '35–45', 'Targeted treatments'), + (Icons.nightlight_round, '45–60', 'Deep renewal & firming'), + (Icons.diamond_rounded, '60+', 'Intensive care & radiance'), + ]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'How old\nare you?', + style: GoogleFonts.plusJakartaSans( + fontSize: 30, + fontWeight: FontWeight.w800, + color: SColors.textPrimary, + height: 1.2, + ), + ).animate().fadeIn(duration: 400.ms), + const SizedBox(height: 8), + Text( + 'Skin changes with age — this helps us tailor advice.', + style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary), + ).animate().fadeIn(duration: 400.ms, delay: 100.ms), + const SizedBox(height: 24), + ...List.generate(_options.length, (i) { + final (icon, label, sub) = _options[i]; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: QuizOptionRow( + icon: icon, + label: label, + sub: sub, + isSelected: selected == label, + onTap: () => onSelect(label), + ).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms) + .slideX(begin: 0.05, end: 0, duration: 300.ms), + ); + }), + ], + ), + ); + } +} + +// ── Step 4: Concerns ────────────────────────────────────────────────────────── + +class _ConcernsStep extends StatelessWidget { + final List selected; + final ValueChanged onToggle; + + const _ConcernsStep({required this.selected, required this.onToggle}); + + static const _options = [ + (Icons.face_retouching_natural, 'Breakouts', 'Acne, blackheads, congestion'), + (Icons.cloud_rounded, 'Dullness', 'Tired, uneven, grey skin'), + (Icons.air_rounded, 'Dryness', 'Tight, rough, flaking'), + (Icons.format_align_justify_rounded, 'Fine lines', 'Wrinkles, loss of firmness'), + (Icons.adjust_rounded, 'Dark spots', 'Hyperpigmentation, melasma'), + (Icons.local_fire_department_rounded, 'Redness', 'Sensitivity, flushing, rosacea'), + ]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your skin\nconcerns?', + style: GoogleFonts.plusJakartaSans( + fontSize: 30, + fontWeight: FontWeight.w800, + color: SColors.textPrimary, + height: 1.2, + ), + ).animate().fadeIn(duration: 400.ms), + const SizedBox(height: 8), + Text( + 'Pick up to 3 that bother you most.', + style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary), + ).animate().fadeIn(duration: 400.ms, delay: 100.ms), + const SizedBox(height: 24), + ...List.generate(_options.length, (i) { + final (icon, label, sub) = _options[i]; + final isSelected = selected.contains(label); + final disabled = !isSelected && selected.length >= 3; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Opacity( + opacity: disabled ? 0.4 : 1.0, + child: QuizOptionRow( + icon: icon, + label: label, + sub: sub, + isSelected: isSelected, + onTap: disabled ? () {} : () => onToggle(label), + ), + ).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms) + .slideX(begin: 0.05, end: 0, duration: 300.ms), + ); + }), + ], + ), + ); + } +} + +// ── Step 5: Goal ────────────────────────────────────────────────────────────── + +class _GoalStep extends StatelessWidget { + final String selected; + final ValueChanged onSelect; + + const _GoalStep({required this.selected, required this.onSelect}); + + static const _options = [ + (Icons.auto_awesome_rounded, 'Healthy glow', 'Radiant, dewy, lit-from-within'), + (Icons.shield_rounded, 'Clear skin', 'Blemish-free, smooth texture'), + (Icons.access_time_rounded, 'Slow aging', 'Firm, youthful, preventive'), + (Icons.palette_rounded, 'Even tone', 'Balanced, bright complexion'), + ]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your skin\ngoal?', + style: GoogleFonts.plusJakartaSans( + fontSize: 30, + fontWeight: FontWeight.w800, + color: SColors.textPrimary, + height: 1.2, + ), + ).animate().fadeIn(duration: 400.ms), + const SizedBox(height: 8), + Text( + 'Pick one — we\'ll build your plan around it.', + style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary), + ).animate().fadeIn(duration: 400.ms, delay: 100.ms), + const SizedBox(height: 24), + ...List.generate(_options.length, (i) { + final (icon, label, sub) = _options[i]; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: QuizOptionRow( + icon: icon, + label: label, + sub: sub, + isSelected: selected == label, + onTap: () => onSelect(label), + ).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms) + .slideX(begin: 0.05, end: 0, duration: 300.ms), + ); + }), + ], + ), + ); + } +} diff --git a/lib/screens/result_screen.dart b/lib/screens/result_screen.dart new file mode 100644 index 0000000..8ee083e --- /dev/null +++ b/lib/screens/result_screen.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../theme.dart'; +import '../widgets/primary_button.dart'; +import 'quiz/quiz_flow.dart'; + +class ResultScreen extends StatefulWidget { + final QuizData data; + const ResultScreen({super.key, required this.data}); + + @override + State createState() => _ResultScreenState(); +} + +class _ResultScreenState extends State { + bool _revealed = false; + + @override + void initState() { + super.initState(); + Future.delayed(const Duration(milliseconds: 1800), () { + if (mounted) setState(() => _revealed = true); + }); + } + + String get _firstName => widget.data.name.split(' ').first; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: _revealed ? _resultView() : _analyzingView(), + ); + } + + Widget _analyzingView() { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF3D32A8), Color(0xFF5B4FCF), Color(0xFF8B5CF6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + strokeWidth: 3, + ) + .animate(onPlay: (c) => c.repeat()) + .rotate(duration: 1.seconds), + const SizedBox(height: 28), + Text( + 'Analysing your skin...', + style: GoogleFonts.plusJakartaSans( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ).animate(onPlay: (c) => c.repeat(reverse: true)) + .fadeIn(duration: 800.ms), + const SizedBox(height: 8), + Text( + 'Building your personalised routine', + style: GoogleFonts.plusJakartaSans( + fontSize: 14, + color: Colors.white.withAlpha(180), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _resultView() { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF3D32A8), Color(0xFF5B4FCF), Color(0xFF8B5CF6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const SizedBox(height: 24), + Image.asset( + 'assets/images/shaynee_hero.png', + height: 180, + fit: BoxFit.contain, + ) + .animate() + .fadeIn(duration: 700.ms) + .slideY(begin: 0.15, end: 0, duration: 700.ms, curve: Curves.easeOut), + const SizedBox(height: 20), + Text( + 'Your skin report\nis ready, $_firstName! ✨', + textAlign: TextAlign.center, + style: GoogleFonts.plusJakartaSans( + fontSize: 28, + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.2, + ), + ) + .animate() + .fadeIn(duration: 600.ms, delay: 200.ms), + const SizedBox(height: 28), + _reportCard() + .animate() + .fadeIn(duration: 600.ms, delay: 400.ms) + .slideY(begin: 0.1, end: 0, duration: 600.ms), + const SizedBox(height: 20), + _routinePreview() + .animate() + .fadeIn(duration: 600.ms, delay: 600.ms), + const SizedBox(height: 28), + PrimaryButton( + label: 'Start my routine', + icon: Icons.arrow_forward_rounded, + onTap: () {}, + ) + .animate() + .fadeIn(duration: 400.ms, delay: 800.ms), + const SizedBox(height: 32), + ], + ), + ), + ), + ); + } + + Widget _reportCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withAlpha(20), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(40)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your skin profile', + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white.withAlpha(160), + letterSpacing: 0.8, + ), + ), + const SizedBox(height: 14), + _reportRow('Skin type', widget.data.skinType), + _reportRow('Age range', widget.data.age), + _reportRow('Concerns', widget.data.concerns.join(', ')), + _reportRow('Main goal', widget.data.goal), + ], + ), + ); + } + + Widget _reportRow(String label, String value) { + if (value.isEmpty) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.plusJakartaSans( + fontSize: 13, + color: Colors.white.withAlpha(160), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: GoogleFonts.plusJakartaSans( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + + Widget _routinePreview() { + const steps = [ + ('🌅', 'Morning', 'Cleanser → Vitamin C → SPF 50'), + ('🌙', 'Evening', 'Cleanser → Retinol → Moisturiser'), + ('✨', 'Weekly', 'Exfoliant + Hydrating mask'), + ]; + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withAlpha(20), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(40)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your routine preview', + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white.withAlpha(160), + letterSpacing: 0.8, + ), + ), + const SizedBox(height: 14), + ...steps.map((s) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(s.$1, style: const TextStyle(fontSize: 20)), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.$2, + style: GoogleFonts.plusJakartaSans( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + Text( + s.$3, + style: GoogleFonts.plusJakartaSans( + fontSize: 12, + color: Colors.white.withAlpha(160), + ), + ), + ], + ), + ], + ), + )), + ], + ), + ); + } +} diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart new file mode 100644 index 0000000..0c21ac3 --- /dev/null +++ b/lib/screens/splash_screen.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../theme.dart'; +import 'intro_screen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + Future.delayed(const Duration(milliseconds: 2600), _goNext); + } + + void _goNext() { + if (!mounted) return; + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (_, a, __) => const IntroScreen(), + transitionsBuilder: (_, a, __, child) => + FadeTransition(opacity: a, child: child), + transitionDuration: const Duration(milliseconds: 500), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF3D32A8), Color(0xFF5B4FCF), Color(0xFF8B5CF6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SafeArea( + child: Column( + children: [ + const Spacer(flex: 2), + Image.asset( + 'assets/images/shaynee_hero.png', + height: 240, + fit: BoxFit.contain, + ) + .animate() + .fadeIn(duration: 700.ms, delay: 200.ms) + .slideY(begin: 0.15, end: 0, duration: 700.ms, curve: Curves.easeOut), + const SizedBox(height: 28), + Text( + 'shaynee', + style: GoogleFonts.plusJakartaSans( + fontSize: 42, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: -1.0, + ), + ) + .animate() + .fadeIn(duration: 600.ms, delay: 500.ms), + const SizedBox(height: 8), + Text( + 'your AI skincare companion', + style: GoogleFonts.plusJakartaSans( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.white.withAlpha(180), + letterSpacing: 0.2, + ), + ) + .animate() + .fadeIn(duration: 600.ms, delay: 700.ms), + const Spacer(flex: 3), + Padding( + padding: const EdgeInsets.only(bottom: 40), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (i) => _dot(i)), + ) + .animate() + .fadeIn(duration: 500.ms, delay: 1200.ms), + ), + ], + ), + ), + ), + ); + } + + Widget _dot(int i) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + width: i == 0 ? 24 : 8, + height: 8, + decoration: BoxDecoration( + color: i == 0 ? Colors.white : Colors.white.withAlpha(80), + borderRadius: BorderRadius.circular(4), + ), + ); + } +} diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100644 index 0000000..2affa14 --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class SColors { + static const primary = Color(0xFF5B4FCF); + static const primaryLight = Color(0xFF7B70E0); + static const primaryDeep = Color(0xFF3D32A8); + static const accent = Color(0xFFFF7EB3); + static const accentSoft = Color(0xFFFFF0F6); + static const bgPage = Color(0xFFFAF9FF); + static const surface = Color(0xFFFFFFFF); + static const textPrimary = Color(0xFF1C1A2E); + static const textSecondary = Color(0xFF7B7A8E); + static const border = Color(0xFFE8E6F0); + static const skyTint = Color(0xFFEEECFA); + static const gradientTop = Color(0xFF5B4FCF); + static const gradientBot = Color(0xFF8B5CF6); +} + +ThemeData buildTheme() { + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: SColors.primary, + brightness: Brightness.light, + ), + scaffoldBackgroundColor: SColors.bgPage, + textTheme: GoogleFonts.plusJakartaSansTextTheme().copyWith( + displayLarge: GoogleFonts.plusJakartaSans( + fontSize: 32, fontWeight: FontWeight.w800, color: SColors.textPrimary, + ), + headlineMedium: GoogleFonts.plusJakartaSans( + fontSize: 24, fontWeight: FontWeight.w700, color: SColors.textPrimary, + ), + bodyLarge: GoogleFonts.plusJakartaSans( + fontSize: 16, fontWeight: FontWeight.w400, color: SColors.textPrimary, + ), + bodyMedium: GoogleFonts.plusJakartaSans( + fontSize: 14, fontWeight: FontWeight.w400, color: SColors.textSecondary, + ), + ), + useMaterial3: true, + ); +} diff --git a/lib/widgets/primary_button.dart b/lib/widgets/primary_button.dart new file mode 100644 index 0000000..7afeb23 --- /dev/null +++ b/lib/widgets/primary_button.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../theme.dart'; + +class PrimaryButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + final bool enabled; + final IconData? icon; + + const PrimaryButton({ + super.key, + required this.label, + required this.onTap, + this.enabled = true, + this.icon, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: enabled ? onTap : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 56, + decoration: BoxDecoration( + gradient: enabled + ? const LinearGradient( + colors: [SColors.primary, SColors.gradientBot], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ) + : null, + color: enabled ? null : SColors.border, + borderRadius: BorderRadius.circular(16), + boxShadow: enabled + ? [ + BoxShadow( + color: SColors.primary.withAlpha(60), + blurRadius: 16, + offset: const Offset(0, 6), + ) + ] + : [], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + label, + style: GoogleFonts.plusJakartaSans( + fontSize: 16, + fontWeight: FontWeight.w700, + color: enabled ? Colors.white : SColors.textSecondary, + ), + ), + if (icon != null) ...[ + const SizedBox(width: 8), + Icon(icon, color: enabled ? Colors.white : SColors.textSecondary, size: 18), + ], + ], + ), + ), + ); + } +} diff --git a/lib/widgets/quiz_option_row.dart b/lib/widgets/quiz_option_row.dart new file mode 100644 index 0000000..f7559cc --- /dev/null +++ b/lib/widgets/quiz_option_row.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../theme.dart'; + +class QuizOptionRow extends StatelessWidget { + final IconData icon; + final String label; + final String sub; + final bool isSelected; + final VoidCallback onTap; + + const QuizOptionRow({ + super.key, + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + this.sub = '', + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + HapticFeedback.selectionClick(); + onTap(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isSelected ? SColors.primary.withAlpha(13) : SColors.surface, + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: isSelected ? SColors.primary.withAlpha(128) : SColors.border, + width: 1.5, + ), + ), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 52, + height: 52, + decoration: BoxDecoration( + color: isSelected + ? SColors.primary.withAlpha(26) + : SColors.skyTint.withAlpha(140), + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + icon, + size: 26, + color: isSelected ? SColors.primary : SColors.textSecondary, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.plusJakartaSans( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isSelected ? SColors.primary : SColors.textPrimary, + ), + ), + if (sub.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + sub, + style: GoogleFonts.plusJakartaSans( + fontSize: 13, + color: SColors.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + const SizedBox(width: 12), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? SColors.primary : Colors.transparent, + border: Border.all( + color: isSelected ? SColors.primary : SColors.border, + width: 1.5, + ), + ), + child: isSelected + ? const Icon(Icons.check, size: 13, color: Colors.white) + : null, + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index fa61e04..ac289ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -33,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85" + url: "https://pub.dev" + source: hosted + version: "1.2.0" collection: dependency: transitive description: @@ -41,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -57,11 +81,27 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_lints: dependency: "direct dev" description: @@ -70,11 +110,67 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" leak_tracker: dependency: transitive description: @@ -107,6 +203,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -131,6 +235,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -139,6 +259,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" sky_engine: dependency: transitive description: flutter @@ -192,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -208,6 +416,30 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.12.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index c8b4e1e..202ccce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,10 +30,9 @@ environment: dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + google_fonts: ^6.2.1 + flutter_animate: ^4.5.0 dev_dependencies: flutter_test: @@ -57,33 +56,5 @@ flutter: # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + assets: + - assets/images/