diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ee0b9d..af38809 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -34,6 +34,7 @@ def keystoreProperties = new Properties() android { compileSdkVersion 33 + ndkVersion '25.1.8937393' sourceSets { main.java.srcDirs += 'src/main/kotlin' } diff --git a/assets/rive/balloon.riv b/assets/rive/balloon.riv new file mode 100644 index 0000000..acb5bdb Binary files /dev/null and b/assets/rive/balloon.riv differ diff --git a/assets/rive/dark.riv b/assets/rive/dark.riv new file mode 100644 index 0000000..1b30d3e Binary files /dev/null and b/assets/rive/dark.riv differ diff --git a/assets/rive/team-work.gif b/assets/rive/team-work.gif new file mode 100644 index 0000000..d3ce288 Binary files /dev/null and b/assets/rive/team-work.gif differ diff --git a/assets/rive/wod.riv b/assets/rive/wod.riv new file mode 100644 index 0000000..4fa8f05 Binary files /dev/null and b/assets/rive/wod.riv differ diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart index c119c72..b13064e 100644 --- a/integration_test/app_test.dart +++ b/integration_test/app_test.dart @@ -4,6 +4,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:navbar_router/navbar_router.dart'; import 'package:vocabhub/main.dart' as app; import 'package:vocabhub/navbar/navbar.dart'; +import 'package:vocabhub/onboarding/onboarding.dart'; import 'package:vocabhub/pages/login.dart'; extension FindText on String { @@ -30,12 +31,62 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding; - group('Test App should load', () { + group('Test App should load:', () { + testWidgets('New User should be onboarded', (WidgetTester tester) async { + await app.main(); + // await binding.convertFlutterSurfaceToImage(); + await Future.delayed(const Duration(seconds: 3)); + await tester.pumpAndSettle(); + expect((app.VocabApp).typeX(), findsOneWidget); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(seconds: 1)); + expect((WelcomePage).typeX(), findsOneWidget); + // final title = "Welcome\nto\nVocabhub".textX(); + // expect(title, findsOneWidget); + await tester.pumpAndSettle(); + final takeATour = "Take a tour".textX(); + expect(takeATour, findsOneWidget); + await tester.pumpAndSettle(); + await tester.tap(takeATour); + await tester.pumpAndSettle(); + expect((OnboardingPage).typeX(), findsOneWidget); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(seconds: 3)); + final title1 = 'A Crowd Sourced platform'.textX(); + expect(title1, findsOneWidget); + final list = List.generate(3, (index) => index).toList(); + double offset = 400; + await for (final item in Stream.fromIterable(list)) { + await tester.timedDragFrom( + Offset(offset, 800), Offset(-offset, 800), Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + offset += 400; + } + // await Future.delayed(const Duration(seconds: 1)); + // final title2 = 'Word of the Day'.textX(); + // expect(title2, findsOneWidget); + // await Future.delayed(const Duration(seconds: 1)); + // await tester.dragFrom(Offset(400, 800), const Offset(-400, 800)); + // // await tester.drag(pageView, const Offset(-400, 400)); + // await tester.pumpAndSettle(); + // final title3 = 'Explore curated words'.textX(); + // expect(title3, findsOneWidget); + // await tester.dragFrom(Offset(400, 800), const Offset(-400, 800)); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(seconds: 1)); + final getStartedText = "Get Started".textX(); + expect(getStartedText, findsOneWidget); + await tester.pumpAndSettle(); + await tester.tap(getStartedText); + await tester.pumpAndSettle(); + expect((AppSignIn).typeX(), findsOneWidget); + }); + testWidgets('User should be able to login', (WidgetTester tester) async { // runZonedGuarded(app.main, (error, stack) { // }); - app.main(); - await binding.convertFlutterSurfaceToImage(); + await app.main(); + // await binding.convertFlutterSurfaceToImage(); await Future.delayed(const Duration(seconds: 3)); await tester.pumpAndSettle(); expect((app.VocabApp).typeX(), findsOneWidget); @@ -81,8 +132,8 @@ void main() { }); testWidgets("User stays loggedIn", (widgetTester) async { - app.main(); - await binding.convertFlutterSurfaceToImage(); + await app.main(); + // await binding.convertFlutterSurfaceToImage(); await Future.delayed(const Duration(seconds: 3)); await widgetTester.pumpAndSettle(); expect((Dashboard).typeX(), findsOneWidget); @@ -102,7 +153,7 @@ void main() { }); testWidgets("Ensure all navbar widgets load", (widgetTester) async { - app.main(); + await app.main(); await binding.convertFlutterSurfaceToImage(); final List baseWidgets = [ Dashboard(), diff --git a/lib/controller/settings_controller.dart b/lib/controller/settings_controller.dart index 5b17771..3ba2043 100644 --- a/lib/controller/settings_controller.dart +++ b/lib/controller/settings_controller.dart @@ -9,9 +9,12 @@ class SettingsController extends ChangeNotifier { ThemeMode get theme => _theme; bool _ratedOnPlayStore = false; DateTime _lastRatedDate = DateTime.now(); + bool _isOnboarded = false; bool get hasRatedOnPlaystore => _ratedOnPlayStore; + bool get isOnboarded => _isOnboarded; + bool get isDark => _theme == ThemeMode.dark; Color _themeSeed = VocabTheme.colorSeeds[1]; String? version; @@ -25,6 +28,11 @@ class SettingsController extends ChangeNotifier { DateTime get lastRatedDate => _lastRatedDate; + Future getOnBoarded() async { + _isOnboarded = await _settingsService!.getOnboarded(); + return _isOnboarded; + } + /// Returns the last rated sheet shown date /// this time does not indicate the user has rated the app /// it only indicates the last time the user was shown the rate sheet @@ -33,6 +41,12 @@ class SettingsController extends ChangeNotifier { return _lastRatedDate; } + set onBoarded(bool value) { + _isOnboarded = value; + notifyListeners(); + _settingsService!.setOnboarded(value); + } + set themeSeed(Color value) { _themeSeed = value; notifyListeners(); @@ -73,6 +87,7 @@ class SettingsController extends ChangeNotifier { _themeSeed = await getThemeSeed(); _ratedOnPlayStore = getRatedOnPlaystore(); _lastRatedDate = await getLastRatedShown(); + _isOnboarded = await getOnBoarded(); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); version = packageInfo.version; notifyListeners(); diff --git a/lib/main.dart b/lib/main.dart index 8d0ff79..bd66cd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; // import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -50,6 +51,14 @@ Future main() async { )); } +class AppScrollBehavior extends MaterialScrollBehavior { + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} + @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { // ignore: avoid_print @@ -143,6 +152,7 @@ class _VocabAppState extends ConsumerState { final colorScheme = ColorScheme.fromSeed(seedColor: settingsController.themeSeed); return MaterialApp( title: Constants.APP_TITLE, + scrollBehavior: AppScrollBehavior(), navigatorObservers: [_observer], debugShowCheckedModeBanner: !kDebugMode, darkTheme: ThemeData.dark( diff --git a/lib/navbar/dashboard/dashboard.dart b/lib/navbar/dashboard/dashboard.dart index 06d2a6c..d45de7f 100644 --- a/lib/navbar/dashboard/dashboard.dart +++ b/lib/navbar/dashboard/dashboard.dart @@ -374,7 +374,6 @@ class DashboardDesktop extends StatelessWidget { ), ), Container( - // height: SizeUtils.size.height * 0.95, padding: EdgeInsets.symmetric(vertical: 16.0), width: 400, child: Column( diff --git a/lib/navbar/profile/settings.dart b/lib/navbar/profile/settings.dart index 62028bc..09be011 100644 --- a/lib/navbar/profile/settings.dart +++ b/lib/navbar/profile/settings.dart @@ -231,6 +231,7 @@ class _SettingsPageMobileState extends ConsumerState { }), hLine(), settingTile('Logout', trailingIcon: Icons.logout, onTap: () async { + user.loggedIn = false; authController.logout(context); Navigate.pushAndPopAll(context, AppSignIn()); }), diff --git a/lib/navbar/search/search.dart b/lib/navbar/search/search.dart index 2efcdb0..7153578 100644 --- a/lib/navbar/search/search.dart +++ b/lib/navbar/search/search.dart @@ -91,7 +91,9 @@ class _MobileViewState extends State { wordsByAlphabet[index].add(element); } }); - showToast('${words.length} Words fetched successfully'); + if (NavbarNotifier.currentIndex == SEARCH_INDEX) { + showToast('${words.length} Words fetched successfully'); + } response.value = response.value.copyWith( data: wordsByAlphabet, state: RequestState.done, diff --git a/lib/onboarding/onboard.dart b/lib/onboarding/onboard.dart new file mode 100644 index 0000000..3affe1f --- /dev/null +++ b/lib/onboarding/onboard.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:navbar_router/navbar_router.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:vocabhub/main.dart'; +import 'package:vocabhub/pages/login.dart'; +import 'package:vocabhub/widgets/button.dart'; + +import 'onboarding.dart'; + +class OnboardingPage extends StatefulWidget { + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State { + int index = 0; + + Future onBoardComplete() async { + setState(() { + isLoading = true; + }); + settingsController.onBoarded = true; + setState(() { + isLoading = false; + }); + Navigate.pushAndPopAll(context, AppSignIn()); + } + + final List pages = [ + OnboardingContentPage( + index: 0, + title: 'A Crowd Sourced platform', + assetPath: 'assets/rive/team-work.gif', + description: 'We are a community of learners, Your contribution helps everyone', + animations: [], + color: Colors.white, + ), + OnboardingContentPage( + index: 1, + title: 'Word of the Day', + assetPath: 'assets/rive/wod.riv', + description: 'Learn a new word everyday throught out the year', + color: Color.fromRGBO(10, 6, 17, 1.0), + animations: [ + 'Animation 1', + ], + ), + // Add your onboarding pages here + OnboardingContentPage( + index: 2, + title: 'Explore curated words', + description: + 'Explore personalized words and definitions everytime you scroll through the explore page', + color: Color(0xffA8C9F8), + assetPath: 'assets/rive/balloon.riv', + animations: [ + 'Balloon Rotation', + 'Cloud Rotation', + 'Cloud 1', + 'Cloud 2', + 'Cloud 3', + 'Cloud 4', + ], + ), + OnboardingContentPage( + index: 3, + title: 'Dark Mode', + assetPath: 'assets/rive/dark.riv', + description: 'Dark mode is here to save your eyes', + animations: [ + 'orbitAnimation', + ], + color: Color(0xff151421), + ), + ]; + bool isLoading = false; + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: PageView.builder( + itemCount: pages.length, + onPageChanged: (x) { + setState(() { + index = x; + }); + }, + itemBuilder: (context, index) { + return pages[index % pages.length]; + // return Stack(children: [ + // pages[index % pages.length], + // Align( + // alignment: Alignment.topCenter, + // child: ClipPath( + // clipper: CircleClipper( + // center: Offset(500, 400), + // radius: 100, + // ), + // child: Container( + // height: 200, + // width: 200, + // color: Colors.red, + // padding: const EdgeInsets.only(top: 100.0), + // child: FlutterLogo(), + // ), + // )), + // ]); + }, + ), + ), + // Align( + // alignment: Alignment.topCenter, + // child: Padding( + // padding: const EdgeInsets.only(top: 100.0), + // child: Container( + // height: 200, + // width: 200, + // color: Colors.amber.withOpacity(0.2), + // padding: const EdgeInsets.only(top: 100.0), + // ), + // )), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 100.0), + child: index == 3 + ? VHButton( + height: 48, + width: 160, + isLoading: isLoading, + onTap: onBoardComplete, + label: 'Get Started') + : SizedBox.shrink(), + )), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 50.0), + child: AnimatedSmoothIndicator( + activeIndex: index, + count: pages.length, + effect: WormEffect(), + ), + ), + ) + ], + )); + } +} + +class ImageHolder extends StatelessWidget { + String imageUrl; + ImageHolder({super.key, required this.imageUrl}); + + @override + Widget build(BuildContext context) { + return ClipPath( + clipBehavior: Clip.hardEdge, + clipper: CircleClipper( + center: Offset(500, 400), + radius: 100, + ), + child: Container( + height: 200, + width: 200, + alignment: Alignment.center, + color: Colors.grey, + )); + } +} + +class SquareClipper extends CustomClipper { + @override + Path getClip(Size size) { + return Path()..addRRect(RRect.fromLTRBR(100, 100, 100, 100, Radius.circular(16.0))); + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + // TODO: implement shouldReclip + return true; + } +} + +class CircleClipper extends CustomClipper { + CircleClipper({required this.center, required this.radius}); + final Offset center; + final double radius; + @override + Path getClip(Size size) { + return Path()..addOval(Rect.fromCircle(radius: radius, center: center)); + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return true; + } +} diff --git a/lib/onboarding/onboard_content.dart b/lib/onboarding/onboard_content.dart new file mode 100644 index 0000000..02f2a02 --- /dev/null +++ b/lib/onboarding/onboard_content.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:navbar_router/navbar_router.dart'; +import 'package:rive/rive.dart'; +import 'package:vocabhub/main.dart'; +import 'package:vocabhub/pages/login.dart'; +import 'package:vocabhub/utils/extensions.dart'; + +class OnboardingContentPage extends StatefulWidget { + final String title; + final String description; + final Color? color; + final int index; + final List animations; + final String assetPath; + + const OnboardingContentPage( + {Key? key, + required this.index, + required this.title, + required this.description, + required this.color, + required this.assetPath, + required this.animations}) + : super(key: key); + + @override + State createState() => _OnboardingContentPageState(); +} + +class _OnboardingContentPageState extends State { + @override + void didUpdateWidget(covariant OnboardingContentPage oldWidget) { + // TODO: implement didUpdateWidget + print(oldWidget.index == widget.index); + super.didUpdateWidget(oldWidget); + } + + late RiveAnimationController _controller; + + // Toggles between play and pause animation states + void _togglePlay() => setState(() => _controller.isActive = !_controller.isActive); + + /// Tracks if the animation is playing by whether controller is running + bool get isPlaying => _controller.isActive; + + @override + void initState() { + _controller = OneShotAnimation('Animation 1'); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return Container( + color: widget.color, + alignment: Alignment.center, + child: Column( + children: [ + 32.0.vSpacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + widget.title, + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: widget.index % 2 == 0 ? Colors.black : Colors.white, + ), + ), + ), + widget.index != 0 + ? SizedBox( + height: size.height * 0.5, + child: RiveAnimation.asset( + '${widget.assetPath}', + animations: widget.animations, + controllers: [_controller], + ), + ) + : SizedBox( + height: size.height * 0.5, + child: Image.asset( + '${widget.assetPath}', + )), + 32.0.vSpacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + widget.description, + style: TextStyle( + fontSize: 18.0, + color: widget.index % 2 == 0 ? Colors.black : Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + 32.0.vSpacer(), + ], + ), + ); + } +} diff --git a/lib/onboarding/onboarding.dart b/lib/onboarding/onboarding.dart new file mode 100644 index 0000000..dbb8703 --- /dev/null +++ b/lib/onboarding/onboarding.dart @@ -0,0 +1,6 @@ +library onboarding; + +export 'onboard.dart'; +export 'onboard_content.dart'; +export 'onboarding.dart'; +export 'welcome.dart'; diff --git a/lib/onboarding/welcome.dart b/lib/onboarding/welcome.dart new file mode 100644 index 0000000..4a1253f --- /dev/null +++ b/lib/onboarding/welcome.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:navbar_router/navbar_router.dart'; +import 'package:vocabhub/pages/login.dart'; +import 'package:vocabhub/themes/vocab_theme.dart'; +import 'package:vocabhub/utils/utils.dart'; +import 'package:vocabhub/widgets/button.dart'; + +import 'onboard.dart'; + +class WelcomePage extends StatefulWidget { + final String title; + final String description; + + const WelcomePage({ + Key? key, + required this.title, + required this.description, + }) : super(key: key); + + @override + State createState() => _WelcomePageState(); +} + +class _WelcomePageState extends State { + Future startOnBoarding() async {} + + bool isLoading = false; + String title = 'Welcome to Vocabhub'; + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final themeData = VocabTheme.getThemeFromSeed(Colors.blue); + return Material( + child: Container( + color: themeData.colorScheme.background, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: title.split(' ')[0], + style: GoogleFonts.quicksand( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 87, 169, 110), + ), + children: [ + TextSpan( + text: '\n${title.split(' ')[1]}', + style: GoogleFonts.quicksand( + fontSize: 40, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + TextSpan( + text: '\n${title.split(' ')[2]}', + style: GoogleFonts.quicksand( + fontSize: 38, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 175, 191, 0), + ), + ) + ]), + ), + ), + Column( + children: [ + VHButton( + width: 160, + onTap: () { + Navigate.push(context, OnboardingPage(), + transitionType: TransitionType.reveal); + }, + label: 'Take a tour'), + 16.0.vSpacer(), + VHButton( + width: 200, + onTap: () { + Navigate.push(context, AppSignIn(), transitionType: TransitionType.scale); + }, + label: 'Skip for now'), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/login.dart b/lib/pages/login.dart index df65317..742f19a 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -47,6 +47,7 @@ class _AppSignInState extends ConsumerState { } else { existingUser.loggedIn = true; _userNotifier.setUser(existingUser); + await AuthService.updateLogin(email: existingUser.email, isLoggedIn: true); _requestNotifier.value = Response(state: RequestState.done); Navigate.pushAndPopAll(context, AdaptiveLayout()); firebaseAnalytics.logSignIn(user!); diff --git a/lib/pages/onboarding/onboarding.dart b/lib/pages/onboarding/onboarding.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/pages/splashscreen.dart b/lib/pages/splashscreen.dart index 220b3d5..48e4f98 100644 --- a/lib/pages/splashscreen.dart +++ b/lib/pages/splashscreen.dart @@ -9,6 +9,8 @@ import 'package:vocabhub/pages/login.dart'; import 'package:vocabhub/utils/settings.dart'; import 'package:vocabhub/utils/size_utils.dart'; +import '../onboarding/welcome.dart'; + class SplashScreen extends ConsumerStatefulWidget { const SplashScreen({Key? key}) : super(key: key); @@ -36,7 +38,16 @@ class _SplashScreenState extends ConsumerState with SingleTickerPr _controller.forward(); _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { - handleNavigation(); + if (settingsController.isOnboarded && SizeUtils.isMobile) { + handleNavigation(); + } else { + Navigate.pushReplace( + context, + WelcomePage( + title: 'Welcome to VocabHub', + description: 'Your companion to learn new words everyday', + )); + } } }); } diff --git a/lib/services/services/auth_service.dart b/lib/services/services/auth_service.dart index a9c53b0..055a8f3 100644 --- a/lib/services/services/auth_service.dart +++ b/lib/services/services/auth_service.dart @@ -5,6 +5,7 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:navbar_router/navbar_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vocabhub/exports.dart'; +import 'package:vocabhub/main.dart'; import 'package:vocabhub/models/models.dart'; import 'package:vocabhub/models/user.dart'; import 'package:vocabhub/services/services/database.dart'; @@ -156,6 +157,7 @@ class AuthService extends ServiceBase { await AuthService.updateLogin(email: user.email, isLoggedIn: false); await googleSignOut(context); await Settings.clear(); + settingsController.onBoarded = true; } @override diff --git a/lib/services/services/settings_service.dart b/lib/services/services/settings_service.dart index 2eb4386..85258c7 100644 --- a/lib/services/services/settings_service.dart +++ b/lib/services/services/settings_service.dart @@ -8,6 +8,7 @@ class SettingsService { final String kThemeSeedKey = 'kThemeSeedKey'; final String kRatedOnPlaystore = 'kRatedOnPlaystore'; final String kLastRatedDate = 'kLastRatedDate'; + final String kOnboardedKey = 'kOnboardedKey'; void setTheme(ThemeMode value) { _sharedPreferences.setBool(kThemeKey, value == ThemeMode.dark); @@ -18,6 +19,14 @@ class SettingsService { return theme == true ? ThemeMode.dark : ThemeMode.light; } + Future setOnboarded(bool value) async { + await _sharedPreferences.setBool(kOnboardedKey, value); + } + + Future getOnboarded() async { + return _sharedPreferences.getBool(kOnboardedKey) ?? false; + } + /// Returns the last rated sheet shown date /// this time does not indicate the user has rated the app /// it only indicates the last time the user was shown the rate sheet diff --git a/lib/themes/vocab_theme.dart b/lib/themes/vocab_theme.dart index ebaf881..45f9992 100644 --- a/lib/themes/vocab_theme.dart +++ b/lib/themes/vocab_theme.dart @@ -57,4 +57,8 @@ class VocabTheme { titleMedium: GoogleFonts.quicksand(fontSize: 20, fontWeight: FontWeight.w300), titleSmall: GoogleFonts.quicksand(fontSize: 16, fontWeight: FontWeight.w300), )); + + static ThemeData getThemeFromSeed(Color seed) { + return ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: seed)); + } } diff --git a/pubspec.yaml b/pubspec.yaml index e1858ca..347c6d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,12 @@ dependencies: realtime_client: ^0.1.15 share_plus: ^4.1.0 shared_preferences: ^2.0.16 + smooth_page_indicator: ^1.1.0 supabase: ^0.3.6 url_launcher: ^6.1.8 uuid: ^3.0.6 webview_flutter: ^4.2.2 + rive: ^0.11.4 webview_flutter_web: ^0.2.2+1 dev_dependencies: @@ -55,40 +57,11 @@ flutter_icons: ios: true image_path: "assets/icon.png" -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: + assets: - assets/ + - assets/rive/ - .env - shorebird.yaml - # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - # 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/custom-fonts/#from-packages