diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..dd4a91f Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c654428 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..ca28e8c 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f909877 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..b94341e 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ffcc6ee Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..b7603ff 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4476cc4 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..98805e7 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..6f73ff2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..f9321a2 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..bd0fdb2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..1e31290 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFA188A6 + \ No newline at end of file diff --git a/assets/app_icon.png b/assets/app_icon.png new file mode 100644 index 0000000..74b20ad Binary files /dev/null and b/assets/app_icon.png differ diff --git a/assets/app_icon1.png b/assets/app_icon1.png new file mode 100644 index 0000000..6bc06a5 Binary files /dev/null and b/assets/app_icon1.png differ diff --git a/assets/app_icon_round.png b/assets/app_icon_round.png new file mode 100644 index 0000000..b6ab00d Binary files /dev/null and b/assets/app_icon_round.png differ diff --git a/assets/splash_icon.png b/assets/splash_icon.png new file mode 100644 index 0000000..5bead22 Binary files /dev/null and b/assets/splash_icon.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..eabd851 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,122 @@ { - "images" : [ + "images": [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "filename": "Icon-App-20x20@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "filename": "Icon-App-20x20@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "filename": "Icon-App-29x29@1x.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "filename": "Icon-App-29x29@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "filename": "Icon-App-29x29@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "filename": "Icon-App-40x40@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "filename": "Icon-App-40x40@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "filename": "Icon-App-60x60@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "filename": "Icon-App-60x60@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "filename": "Icon-App-20x20@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "filename": "Icon-App-20x20@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "filename": "Icon-App-29x29@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "filename": "Icon-App-29x29@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "filename": "Icon-App-40x40@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "filename": "Icon-App-40x40@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "filename": "Icon-App-76x76@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "filename": "Icon-App-76x76@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "filename": "Icon-App-83.5x83.5@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "filename": "Icon-App-1024x1024@1x.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "author": "icons_launcher", + "version": 1 } -} +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..d275a09 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41..c2b159c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452..8193ea0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d93..b19fcea 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b00..27da476 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe73094..732620d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773c..399aeab 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452..8193ea0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463..e99c025 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec3034..815298c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec3034..815298c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea..d242a73 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32a..3a5c12c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba..2a0280d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf1..1d94fda 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/features/account/account_create_screen.dart b/lib/features/account/account_create_screen.dart new file mode 100644 index 0000000..a61f7e9 --- /dev/null +++ b/lib/features/account/account_create_screen.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:rluv/features/account/signup.dart'; +import 'package:rluv/global/styles.dart'; + +import '../../global/api.dart'; +import '../../global/widgets/ui_button.dart'; +import '../../main.dart'; +import 'login.dart'; + +class AccountCreateScreen extends ConsumerStatefulWidget { + const AccountCreateScreen({super.key}); + + @override + ConsumerState createState() => + _AccountCreateScreenState(); +} + +enum _AccountScreen { options, login, signup } + +class _AccountCreateScreenState extends ConsumerState { + _AccountScreen currentScreen = _AccountScreen.options; + static final signupFormKey = GlobalKey(); + static final loginFormKey = GlobalKey(); + + bool usingUsername = true; + bool hasFamilyCode = false; + + @override + Widget build(BuildContext context) { + if (ref.watch(tokenProvider) != null) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (ctx) => const Home()), + (r) => false, + ); + }, + ); + } + final screen = BuildMedia(context).size; + return Scaffold( + backgroundColor: Styles.purpleNurple, + body: SafeArea( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: currentScreen == _AccountScreen.options + ? Center( + child: SizedBox( + width: screen.width * 0.5 > 400 ? 400 : screen.width * 0.5, + child: Column( + children: [ + const Spacer(), + const Text( + 'Welcome!', + style: TextStyle(fontSize: 28), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Image.asset("assets/app_icon.png", + height: + screen.width > 500 ? 250 : screen.width * 0.5, + width: screen.width > 500 + ? 250 + : screen.width * 0.5), + ), + UiButton( + color: Styles.sunflower, + onPressed: () { + setState( + () => currentScreen = _AccountScreen.signup, + ); + }, + text: 'Signup', + ), + const SizedBox(height: 20), + UiButton( + color: Styles.flounderBlue, + onPressed: () { + setState( + () => currentScreen = _AccountScreen.login, + ); + }, + text: 'Login', + ), + const Spacer(), + ], + ), + ), + ) + : currentScreen == _AccountScreen.login + ? Login(formKey: loginFormKey, exitNav: exitNav()) + : Signup( + formKey: signupFormKey, + exitNav: exitNav(), + ), + ), + ), + ); + } + + Widget exitNav() => Align( + alignment: Alignment.topLeft, + child: IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: () { + setState( + () => currentScreen = _AccountScreen.options, + ); + }, + ), + ); +} diff --git a/lib/features/account/login.dart b/lib/features/account/login.dart new file mode 100644 index 0000000..bc834b2 --- /dev/null +++ b/lib/features/account/login.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:helpers/helpers/print.dart'; + +import '../../global/api.dart'; +import '../../global/styles.dart'; +import '../../global/utils.dart'; +import '../../global/widgets/ui_button.dart'; + +class Login extends ConsumerStatefulWidget { + const Login({super.key, required this.formKey, required this.exitNav}); + + final GlobalKey formKey; + final Widget exitNav; + + @override + ConsumerState createState() => _LoginState(); +} + +class _LoginState extends ConsumerState { + bool usingUsername = false; + final emailController = TextEditingController(); + final usernameController = TextEditingController(); + final passwordController = TextEditingController(); + + final focusNodes = [ + FocusNode(), + FocusNode(), + FocusNode(), + ]; + + @override + Widget build(BuildContext context) { + final screen = BuildMedia(context).size; + return Form( + key: widget.formKey, + child: Stack( + children: [ + Column( + children: [ + const Spacer(), + const Text( + 'Login', + style: TextStyle(fontSize: 24), + ), + const Spacer(flex: 2), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + const Padding( + padding: EdgeInsets.all(2.0), + child: Text( + 'Username', + style: TextStyle( + fontSize: 16, + ), + ), + ), + Checkbox( + activeColor: Styles.lavender, + onChanged: (bool? value) { + setState(() => usingUsername = true); + }, + value: usingUsername, + ), + ], + ), + const SizedBox( + width: 35, + ), + Column( + children: [ + const Padding( + padding: EdgeInsets.all(2.0), + child: Text( + 'Email', + style: TextStyle( + fontSize: 16, + ), + ), + ), + Checkbox( + activeColor: Styles.lavender, + onChanged: (bool? value) { + setState(() => usingUsername = false); + }, + value: !usingUsername, + ), + ], + ), + ], + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: !usingUsername + ? generateTextField( + controller: emailController, + size: screen, + text: 'Email: ', + validatorFunc: (s) { + if (s != null && s.isNotEmpty && !isEmailValid(s)) { + return 'Email entered is invalid'; + } + return null; + }, + index: 0) + : generateTextField( + controller: usernameController, + size: screen, + text: 'Username: ', + validatorFunc: (s) { + if (s == null || s.isEmpty) { + return 'Invalid Username'; + } + if (s.length < 3) { + return 'Username Not 3 Letters Long'; + } + if (s.length > 20) { + return 'Username Too Long'; + } + if (!isUsernameValid(s)) { + return 'Letters, Numbers, and ., -, _ Allowed'; + } + return null; + }, + index: 0), + ), + generateTextField( + controller: passwordController, + size: screen, + text: 'Password: ', + validatorFunc: (s) { + if (s != null && s.length < 6) { + return 'Please do a better password (you have to)'; + } + return null; + }, + index: 1, + isPassword: true), + const Spacer(flex: 2), + UiButton( + width: screen.width * 0.85 > 400 ? 400 : screen.width * 0.85, + showLoading: true, + onPressed: () => login(), + text: 'Submit', + color: Styles.seaweedGreen, + ), + const Spacer(), + ], + ), + widget.exitNav, + ], + ), + ); + } + + Widget generateTextField({ + required String text, + String? label, + required int index, + required TextEditingController controller, + isPassword = false, + required Size size, + required String? Function(String?) validatorFunc, + }) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + style: TextStyle(fontSize: size.width < 350 ? 16 : 20), + ), + ), + const Spacer(), + Container( + decoration: Styles.boxLavenderBubble, + width: size.width * 0.5 > 300 ? 300 : size.width * 0.5, + child: TextFormField( + validator: validatorFunc, + controller: controller, + decoration: Styles.inputLavenderBubble(labelText: label), + obscureText: isPassword, + focusNode: focusNodes[index], + style: const TextStyle(fontSize: 16), + onFieldSubmitted: (_) { + if (index != focusNodes.length - 1) { + focusNodes[index + 1].requestFocus(); + } + }, + ), + ), + SizedBox( + width: size.width * 0.05, + ), + ], + ), + ); + } + + Future login() async { + try { + if (widget.formKey.currentState != null && + !widget.formKey.currentState!.validate()) { + return; + } + final data = + await ref.read(apiProvider.notifier).post(path: 'auth/login', data: { + 'username': + usernameController.text.isEmpty ? null : usernameController.text, + 'email': emailController.text.isEmpty ? null : emailController.text, + 'password': passwordController.text, + }); + + final bool success = data?['success'] ?? false; + showSnack( + ref: ref, + text: data?['message'] ?? success + ? 'Login successful' + : 'Login unsuccessful', + type: !success ? SnackType.error : SnackType.success); + printAmber(data); + } catch (err, st) { + printRed('Error in login: $err\n$st'); + } + } +} diff --git a/lib/features/account/signup.dart b/lib/features/account/signup.dart new file mode 100644 index 0000000..b7075fa --- /dev/null +++ b/lib/features/account/signup.dart @@ -0,0 +1,250 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:helpers/helpers/print.dart'; + +import '../../global/api.dart'; +import '../../global/styles.dart'; +import '../../global/utils.dart'; +import '../../global/widgets/ui_button.dart'; + +class Signup extends ConsumerStatefulWidget { + const Signup({super.key, required this.formKey, required this.exitNav}); + + final GlobalKey formKey; + final Widget exitNav; + + @override + ConsumerState createState() => _SignupState(); +} + +class _SignupState extends ConsumerState { + bool hasFamilyCode = false; + final nameCotroller = TextEditingController(); + final usernameController = TextEditingController(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final familyCodeController = TextEditingController(); + + final focusNodes = [ + FocusNode(), + FocusNode(), + FocusNode(), + FocusNode(), + FocusNode(), + ]; + + @override + Widget build(BuildContext context) { + final screen = BuildMedia(context).size; + return Form( + key: widget.formKey, + child: Stack( + children: [ + Column( + children: [ + const Spacer(), + const Text( + 'Signup', + style: TextStyle(fontSize: 24), + ), + const Spacer(), + generateTextField( + controller: nameCotroller, + size: screen, + text: 'Name:', + validatorFunc: (s) { + if (s == null || s.isEmpty) { + return 'You matter! Enter a name :)'; + } + return null; + }, + index: 0), + generateTextField( + controller: usernameController, + size: screen, + text: 'Username: ', + validatorFunc: (s) { + if (s == null || s.isEmpty) { + return 'Invalid Username'; + } + if (s.length < 3) { + return 'Username Not 3 Letters Long'; + } + if (s.length > 20) { + return 'Username Too Long'; + } + if (!isUsernameValid(s)) { + return 'Letters, Numbers, and ., -, _ Allowed'; + } + return null; + }, + index: 1), + generateTextField( + controller: emailController, + size: screen, + validatorFunc: (s) { + if (s != null && s.isNotEmpty && !isEmailValid(s)) { + return 'Email entered is invalid'; + } + return null; + }, + text: 'Email: (optional)', + index: 2), + generateTextField( + controller: passwordController, + size: screen, + validatorFunc: (s) { + if (s != null && s.length < 6) { + return 'Please do a better password (you have to)'; + } + return null; + }, + text: 'Password:', + index: 3, + isPassword: true), + const SizedBox(height: 30), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: hasFamilyCode + ? Column( + children: [ + generateTextField( + controller: familyCodeController, + size: screen, + validatorFunc: (s) { + if (hasFamilyCode) { + if (s == null || s.length != 5) { + return 'Invalid Code'; + } + } + return null; + }, + text: 'Family Code:', + index: 4, + ), + UiButton( + width: screen.width * 0.4, + icon: const Icon(Icons.cancel, + color: Styles.washedStone, size: 20), + onPressed: () { + setState( + () => hasFamilyCode = false, + ); + }, + text: 'Cancel', + ), + ], + ) + : Padding( + padding: const EdgeInsets.all(8.0), + child: UiButton( + width: screen.width * 0.5 > 400 + ? 400 + : screen.width * 0.5, + text: 'JOIN FAMILY', + // color: Styles.flounderBlue, + onPressed: () { + setState( + () => hasFamilyCode = true, + ); + }, + ), + )), + const Spacer(), + UiButton( + showLoading: true, + onPressed: () => signup(), + text: 'Submit', + color: Styles.seaweedGreen, + ), + const Spacer(), + ], + ), + widget.exitNav, + ], + ), + ); + } + + Widget generateTextField({ + required String text, + String? label, + required int index, + required TextEditingController controller, + isPassword = false, + required Size size, + required String? Function(String?) validatorFunc, + }) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + style: TextStyle(fontSize: size.width < 350 ? 16 : 20), + ), + ), + const Spacer(), + Container( + decoration: Styles.boxLavenderBubble, + width: size.width * 0.5 > 300 ? 300 : size.width * 0.5, + child: TextFormField( + validator: validatorFunc, + controller: controller, + decoration: Styles.inputLavenderBubble(labelText: label), + obscureText: isPassword, + focusNode: focusNodes[index], + style: const TextStyle(fontSize: 16), + onFieldSubmitted: (_) { + if (index != focusNodes.length - 1) { + focusNodes[index + 1].requestFocus(); + } + }, + ), + ), + SizedBox( + width: size.width * 0.05, + ), + ], + ), + ); + } + + Future signup() async { + try { + if (widget.formKey.currentState != null && + !widget.formKey.currentState!.validate()) { + return; + } + final data = + await ref.read(apiProvider.notifier).post(path: 'auth/signup', data: { + 'name': nameCotroller.text, + 'username': + usernameController.text.isEmpty ? null : usernameController.text, + 'email': emailController.text.isEmpty ? null : emailController.text, + 'password': passwordController.text, + 'family_code': familyCodeController.text.isEmpty + ? null + : familyCodeController.text.toUpperCase(), + 'budget_name': 'Budget' + }); + + final success = data?['success'] ?? false; + showSnack( + ref: ref, + text: data?['message'] ?? success + ? 'Login successful' + : 'Login unsuccessful', + type: !success ? SnackType.error : SnackType.success); + + printAmber(data); + // final user = User.fromJson(data?['user']); + // ref.read(tokenProvider.notifier).state = + } catch (err) { + printRed(err); + } + } +} diff --git a/lib/features/budget/screens/budget_overview.dart b/lib/features/budget/screens/budget_overview.dart index d02f703..c26b74f 100644 --- a/lib/features/budget/screens/budget_overview.dart +++ b/lib/features/budget/screens/budget_overview.dart @@ -26,256 +26,285 @@ class _BudgetOverviewScreenState extends ConsumerState { final budgetListScrollController = ScrollController(); @override Widget build(BuildContext context) { - final budgetCategories = ref.watch(Store().budgetCategoriesProvider); - final transactions = ref.watch(Store().transactionsProvider); + final budgetCategories = ref.watch(budgetCategoriesProvider); + final transactions = ref.watch(transactionsProvider); final screen = BuildMedia(context).size; double netExpense = 0.0; double netIncome = 0.0; Map budgetCategoryNetMap = {}; - netExpense = transactions - .where((t) => t.type == TransactionType.expense) - .fold(netExpense, (net, t) => net + t.amount); - netIncome = transactions - .where((t) => t.type == TransactionType.income) - .fold(netIncome, (net, t) => net + t.amount); - for (final bud in budgetCategories) { - double net = 0.0; - net = transactions - .where((t) => t.budgetCategoryId == bud.id) - .fold(net, (net, t) => net + t.amount); - budgetCategoryNetMap[bud.id!] = net; - } - return RefreshIndicator( - displacement: 20.0, - onRefresh: () async { - final family = ref.read(Store().familyProvider).valueOrNull; - final _ = - ref.read(Store().dashboardProvider.notifier).fetchDashboard(family); - }, - child: Column( - children: [ - /// TOP HALF, TITLE & OVERVIEW - Container( - height: screen.height * 0.3, - width: screen.width, - color: Styles.purpleNurple, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(flex: 2), - Text( - formatDate(DateTime.now()), - style: - const TextStyle(fontSize: 16, color: Styles.electricBlue), - ), - const Spacer(), - const Text('MONTHLY', - style: TextStyle(fontSize: 42, color: Styles.electricBlue)), - const Spacer(flex: 2), - BudgetNetBar(isPositive: true, net: netIncome), - const Spacer(), - BudgetNetBar(isPositive: false, net: netExpense), - const Spacer(), - ], - ), - ), - - /// BOTTOM HALF, BUDGET BREAKDOWN - Expanded( - child: Container( - color: Styles.sunflower, + netExpense = transactions + .where((t) => t.type == TransactionType.expense) + .fold(netExpense, (net, t) => net + t.amount); + netIncome = transactions + .where((t) => t.type == TransactionType.income) + .fold(netIncome, (net, t) => net + t.amount); + for (final bud in budgetCategories) { + double net = 0.0; + net = transactions + .where((t) => t.budgetCategoryId == bud.id) + .fold(net, (net, t) => net + t.amount); + budgetCategoryNetMap[bud.id!] = net; + } + return Stack( + children: [ + Column( + children: [ + /// TOP HALF, TITLE & OVERVIEW + Container( + height: screen.height * 0.3, width: screen.width, + color: Styles.purpleNurple, child: Column( - mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(14.0), - child: Container( - decoration: BoxDecoration( - color: Styles.blushingPink, - borderRadius: BorderRadius.circular(16.0), - ), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'BUDGET', - style: TextStyle( - fontSize: 28, color: Styles.electricBlue), - ), - ), - budgetCategories.isEmpty - ? Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: screen.width * 0.8, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'No budget categories created yet, add some!'), - ), - ElevatedButton( - onPressed: ref.watch( - Store().budgetProvider) == - null - ? null - : () => showDialog( - context: context, - builder: (context) => Dialog( - shape: - Styles.dialogShape, - backgroundColor: - Styles.dialogColor, - child: - const AddBudgetCategoryDialog()), - ), - child: const Text('Add Category'), - ), - ], - ), - ), - ) - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: 2.0, vertical: 4.0), - child: SizedBox( - height: screen.height * 0.36, - child: Scrollbar( - controller: budgetListScrollController, - thumbVisibility: true, - child: ListView( - physics: const BouncingScrollPhysics(), - controller: budgetListScrollController, - shrinkWrap: true, - children: [ - ...budgetCategories - .mapIndexed((i, category) { - return BudgetCategoryBar( - budgetCategory: category, - currentAmount: - budgetCategoryNetMap[ - category.id]!, - index: i, - ); - }).toList(), - const SizedBox(height: 20), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SizedBox( - width: 140, - child: ElevatedButton( - onPressed: ref.watch(Store() - .budgetProvider) == - null - ? null - : () => showDialog( - context: context, - builder: (context) => Dialog( - shape: Styles - .dialogShape, - backgroundColor: - Styles - .dialogColor, - child: - const AddBudgetCategoryDialog()), - ), - child: const Text( - 'Add Category'), - ), - ), - ], - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ), - ], - ), - ), + const Spacer(flex: 2), + Text( + formatDate(DateTime.now()), + style: const TextStyle( + fontSize: 16, color: Styles.electricBlue), ), const Spacer(), - Row( - children: [ - Expanded( - child: Padding( - padding: - const EdgeInsets.only(left: 20.0, right: 15.0), - child: Container( - height: 70, - decoration: BoxDecoration( - color: Styles.seaweedGreen, - borderRadius: BorderRadius.circular(15.0), - ), - child: InkWell( - child: const Center( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 20.0, vertical: 14.0), - child: FittedBox( - child: Text( - 'Transaction History', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 25), - ), - ), - ), - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const TransactionsListview())); - }, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: Container( - decoration: BoxDecoration( - color: Styles.purpleNurple, - borderRadius: BorderRadius.circular(40.0), - ), - height: 80, - width: 80, - child: IconButton( - icon: const Icon( - Icons.add, - size: 48, - color: Styles.lavender, - ), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return Dialog( - backgroundColor: Styles.dialogColor, - shape: Styles.dialogShape, - child: const AddTransactionDialog()); - }, - ); - }, - ), - ), - ), - ], - ), + const Text('MONTHLY', + style: + TextStyle(fontSize: 42, color: Styles.electricBlue)), + const Spacer(flex: 2), + BudgetNetBar(isPositive: true, net: netIncome), + const Spacer(), + BudgetNetBar(isPositive: false, net: netExpense), const Spacer(), ], ), ), - ) - ], - ), + + /// BOTTOM HALF, BUDGET BREAKDOWN + Expanded( + child: Container( + color: Styles.sunflower, + width: screen.width, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(14.0), + child: Container( + decoration: BoxDecoration( + color: Styles.blushingPink, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'BUDGET', + style: TextStyle( + fontSize: 28, color: Styles.electricBlue), + ), + ), + budgetCategories.isEmpty + ? Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: screen.width * 0.8, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'No budget categories created yet, add some!'), + ), + ElevatedButton( + onPressed: + ref.watch(budgetProvider) == + null + ? null + : () => showDialog( + context: context, + builder: (context) => Dialog( + shape: Styles + .dialogShape, + backgroundColor: + Styles + .dialogColor, + child: + const AddBudgetCategoryDialog()), + ), + child: const Text('Add Category'), + ), + ], + ), + ), + ) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2.0, vertical: 4.0), + child: SizedBox( + height: screen.height * 0.36, + child: Scrollbar( + controller: budgetListScrollController, + thumbVisibility: true, + child: ListView( + physics: + const BouncingScrollPhysics(), + controller: + budgetListScrollController, + shrinkWrap: true, + children: [ + ...budgetCategories + .mapIndexed((i, category) { + return BudgetCategoryBar( + budgetCategory: category, + currentAmount: + budgetCategoryNetMap[ + category.id]!, + index: i, + ); + }).toList(), + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SizedBox( + width: 140, + child: ElevatedButton( + onPressed: ref.watch( + budgetProvider) == + null + ? null + : () => showDialog( + context: context, + builder: (context) => Dialog( + shape: Styles + .dialogShape, + backgroundColor: + Styles + .dialogColor, + child: + const AddBudgetCategoryDialog()), + ), + child: const Text( + 'Add Category'), + ), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ], + ), + ), + ), + const Spacer(), + Row( + children: [ + Expanded( + child: Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 15.0), + child: Container( + height: 70, + decoration: BoxDecoration( + color: Styles.seaweedGreen, + borderRadius: BorderRadius.circular(15.0), + ), + child: InkWell( + child: const Center( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.0, vertical: 14.0), + child: FittedBox( + child: Text( + 'Transaction History', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 25), + ), + ), + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const TransactionsListview())); + }, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: Container( + decoration: BoxDecoration( + color: Styles.purpleNurple, + borderRadius: BorderRadius.circular(40.0), + ), + height: 80, + width: 80, + child: IconButton( + icon: const Icon( + Icons.add, + size: 48, + color: Styles.lavender, + ), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Styles.dialogColor, + shape: Styles.dialogShape, + child: const AddTransactionDialog()); + }, + ); + }, + ), + ), + ), + ], + ), + const Spacer(), + ], + ), + ), + ) + ], + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(top: 5.0, right: 5.0), + child: IconButton( + icon: const Icon(Icons.loop), + color: Styles.seaweedGreen, + onPressed: () => + ref.read(dashboardProvider.notifier).fetchDashboard(), + ), + ), + ), + IgnorePointer( + child: AnimatedOpacity( + duration: const Duration(milliseconds: 150), + opacity: ref.watch(loadingStateProvider) ? 0.75 : 0.0, + child: Container( + color: Colors.black12, + height: screen.height, + width: screen.width, + child: const Center( + child: CircularProgressIndicator( + color: Styles.lavender, + strokeWidth: 2.5, + ), + ), + ), + ), + ) + ], ); } } diff --git a/lib/features/budget/screens/transactions_listview.dart b/lib/features/budget/screens/transactions_listview.dart index 8adc0c2..14afc2b 100644 --- a/lib/features/budget/screens/transactions_listview.dart +++ b/lib/features/budget/screens/transactions_listview.dart @@ -16,7 +16,7 @@ class TransactionsListview extends ConsumerStatefulWidget { class _TransactionsListviewState extends ConsumerState { @override Widget build(BuildContext context) { - final transactions = ref.watch(Store().transactionsProvider); + final transactions = ref.watch(transactionsProvider); transactions.sort( (a, b) => b.date.compareTo(a.date), ); diff --git a/lib/features/budget/widgets/add_budget_category_dialog.dart b/lib/features/budget/widgets/add_budget_category_dialog.dart index 3846bd2..840ef0b 100644 --- a/lib/features/budget/widgets/add_budget_category_dialog.dart +++ b/lib/features/budget/widgets/add_budget_category_dialog.dart @@ -8,6 +8,8 @@ import 'package:rluv/models/budget_category_model.dart'; import '../../../global/api.dart'; import '../../../global/store.dart'; +import '../../../global/utils.dart'; +import '../../../global/widgets/ui_button.dart'; class AddBudgetCategoryDialog extends ConsumerStatefulWidget { const AddBudgetCategoryDialog({super.key}); @@ -19,8 +21,6 @@ class AddBudgetCategoryDialog extends ConsumerStatefulWidget { class _AddBudgetCategoryDialogState extends ConsumerState { - bool loading = false; - bool complete = false; late final Budget? budget; final categoryNameController = TextEditingController(); final amountController = TextEditingController(); @@ -34,7 +34,7 @@ class _AddBudgetCategoryDialogState final formKey = GlobalKey(); @override void initState() { - budget = ref.read(Store().budgetProvider); + budget = ref.read(budgetProvider); WidgetsBinding.instance.addPostFrameCallback((_) { categoryFocusNode.requestFocus(); }); @@ -46,20 +46,10 @@ class _AddBudgetCategoryDialogState if (budget == null) { WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Could not get your budget'), - ), - ); - }); - return Container(); - } else if (complete) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Budget Category Added!'), - ), + showSnack( + ref: ref, + text: 'Could not get your budget', + type: SnackType.error, ); }); return Container(); @@ -215,10 +205,20 @@ class _AddBudgetCategoryDialogState ), ), ), - ElevatedButton( - onPressed: () => submitCategory(colors[selectedColorIndex]), - child: const Text('SAVE'), - ) + UiButton( + showLoading: true, + color: Styles.lavender, + text: 'SAVE', + onPressed: () => + submitCategory(colors[selectedColorIndex]).then((_) { + Navigator.pop(context); + showSnack( + ref: ref, + text: 'Budget Category Added!', + type: SnackType.success, + ); + }), + ), ], ), ), @@ -227,7 +227,6 @@ class _AddBudgetCategoryDialogState icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), - if (loading) const Center(child: CircularProgressIndicator()), ], ), ); @@ -238,9 +237,6 @@ class _AddBudgetCategoryDialogState printPink('Failed validation'); return; } - setState( - () => loading = true, - ); final newBudget = BudgetCategory( id: null, amount: double.parse(amountController.text), @@ -249,17 +245,16 @@ class _AddBudgetCategoryDialogState name: categoryNameController.text, ); - final budgetData = - await Api().post(path: 'budget_category', data: newBudget.toJson()); + final budgetData = await ref + .read(apiProvider.notifier) + .post(path: 'budget_category', data: newBudget.toJson()); final success = budgetData != null ? budgetData['success'] as bool : false; if (success) { ref - .read(Store().dashboardProvider.notifier) + .read(dashboardProvider.notifier) .add({'budget_categories': budgetData}); + } else { + showSnack(ref: ref, text: 'Could not add budget', type: SnackType.error); } - complete = true; - setState(() { - loading = false; - }); } } diff --git a/lib/features/budget/widgets/add_transaction_dialog.dart b/lib/features/budget/widgets/add_transaction_dialog.dart index 92fa676..5466350 100644 --- a/lib/features/budget/widgets/add_transaction_dialog.dart +++ b/lib/features/budget/widgets/add_transaction_dialog.dart @@ -2,10 +2,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:rluv/global/utils.dart'; import '../../../global/api.dart'; import '../../../global/store.dart'; import '../../../global/styles.dart'; +import '../../../global/widgets/ui_button.dart'; import '../../../models/budget_category_model.dart'; import '../../../models/transaction_model.dart'; @@ -20,8 +22,7 @@ class AddTransactionDialog extends ConsumerStatefulWidget { } class _AddTransactionDialogState extends ConsumerState { - bool loading = false; - bool complete = false; + late DateTime selectedDate; late final TextEditingController amountController; late final TextEditingController memoController; @@ -38,11 +39,13 @@ class _AddTransactionDialogState extends ConsumerState { TextEditingController(text: widget.transaction!.amount.toString()); memoController = TextEditingController(text: widget.transaction!.memo); transactionType = widget.transaction!.type; + selectedDate = widget.transaction!.date; } else { amountController = TextEditingController(); memoController = TextEditingController(); + selectedDate = DateTime.now(); } - final categories = ref.read(Store().budgetCategoriesProvider); + final categories = ref.read(budgetCategoriesProvider); if (categories.isNotEmpty) { selectedBudgetCategory = categories.first; } @@ -51,21 +54,8 @@ class _AddTransactionDialogState extends ConsumerState { @override Widget build(BuildContext context) { - if (complete) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(widget.transaction != null - ? 'Transaction updated!' - : 'Transaction added!'), - ), - ); - }); - return Container(); - } final List budgetCategories = - ref.read(Store().budgetCategoriesProvider); + ref.read(budgetCategoriesProvider); return SizedBox( width: BuildMedia(context).width * Styles.dialogScreenWidthFactor, child: budgetCategories.isEmpty @@ -260,9 +250,20 @@ class _AddTransactionDialogState extends ConsumerState { onFieldSubmitted: (_) {}, ), ), - ElevatedButton( - onPressed: loading ? null : () => submitTransaction(), - child: const Text('Add'), + UiButton( + showLoading: true, + text: 'ADD', + color: Styles.lavender, + onPressed: () => submitTransaction().then((_) { + Navigator.pop(context); + showSnack( + ref: ref, + text: widget.transaction != null + ? 'Transaction updated!' + : 'Transaction added!', + type: SnackType.success, + ); + }), ), ], ), @@ -270,12 +271,9 @@ class _AddTransactionDialogState extends ConsumerState { } Future submitTransaction() async { - setState(() { - loading = true; - }); Map? data; if (widget.transaction != null) { - data = await Api().put( + data = await ref.read(apiProvider.notifier).put( path: 'transactions', data: Transaction.copyWith(widget.transaction!, { 'memo': memoController.text.isNotEmpty ? memoController.text : null, @@ -285,7 +283,7 @@ class _AddTransactionDialogState extends ConsumerState { : selectedBudgetCategory.id, }).toJson()); } else { - data = await Api().post( + data = await ref.read(apiProvider.notifier).post( path: 'transactions', data: Transaction( amount: double.parse(amountController.text), @@ -304,26 +302,17 @@ class _AddTransactionDialogState extends ConsumerState { final success = data != null ? data['success'] as bool : false; if (success) { if (widget.transaction != null) { - ref - .read(Store().dashboardProvider.notifier) - .update({'transactions': data}); + ref.read(dashboardProvider.notifier).update({'transactions': data}); } else { - ref - .read(Store().dashboardProvider.notifier) - .add({'transactions': data}); + ref.read(dashboardProvider.notifier).add({'transactions': data}); } - complete = true; } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(widget.transaction != null + showSnack( + ref: ref, + text: widget.transaction != null ? 'Failed to edit transaction' - : 'Failed to add transaction'), - ), - ); + : 'Failed to add transaction', + type: SnackType.error); } - setState(() { - loading = false; - }); } } diff --git a/lib/features/budget/widgets/budget_category_bar.dart b/lib/features/budget/widgets/budget_category_bar.dart index 11d25a7..5dea869 100644 --- a/lib/features/budget/widgets/budget_category_bar.dart +++ b/lib/features/budget/widgets/budget_category_bar.dart @@ -14,7 +14,7 @@ class BudgetCategoryBar extends StatefulWidget { required this.currentAmount, required this.index, this.height = 32, - this.innerPadding = 1.5}); + this.innerPadding = 2.5}); final int index; final double currentAmount; @@ -27,6 +27,7 @@ class BudgetCategoryBar extends StatefulWidget { class _BudgetCategoryBarState extends State { double percentSpent = 0.0; + @override void initState() { Future.delayed(Duration(milliseconds: min(1600, 200 * widget.index)), () { @@ -76,7 +77,7 @@ class _BudgetCategoryBarState extends State { height: widget.height, decoration: BoxDecoration( color: Colors.black, - borderRadius: BorderRadius.circular(14.0), + borderRadius: BorderRadius.circular(13.0), ), ), Padding( diff --git a/lib/features/budget/widgets/transaction_list_item.dart b/lib/features/budget/widgets/transaction_list_item.dart index 8bbb406..86604f7 100644 --- a/lib/features/budget/widgets/transaction_list_item.dart +++ b/lib/features/budget/widgets/transaction_list_item.dart @@ -35,8 +35,8 @@ class _TransactionListItemState extends ConsumerState { @override Widget build(BuildContext context) { - final transaction = ref.watch(Store().transactionsProvider)[widget.index]; - final budgetCategories = ref.read(Store().budgetCategoriesProvider); + final transaction = ref.watch(transactionsProvider)[widget.index]; + final budgetCategories = ref.read(budgetCategoriesProvider); if (transaction.type == TransactionType.expense) { budgetCategory = budgetCategories.singleWhere( (category) => category.id == transaction.budgetCategoryId, diff --git a/lib/features/notes/screens/notes_screen.dart b/lib/features/notes/screens/notes_screen.dart index 02add73..cd47158 100644 --- a/lib/features/notes/screens/notes_screen.dart +++ b/lib/features/notes/screens/notes_screen.dart @@ -1,8 +1,15 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:helpers/helpers/print.dart'; +import 'package:rluv/global/utils.dart'; +import 'package:rluv/models/shared_note.dart'; +import '../../../global/api.dart'; import '../../../global/store.dart'; +import '../../../global/styles.dart'; +import '../../../global/widgets/ui_button.dart'; class SharedNotesScreen extends ConsumerStatefulWidget { const SharedNotesScreen({super.key, required this.initialData}); @@ -15,7 +22,8 @@ class SharedNotesScreen extends ConsumerStatefulWidget { class _SharedNotesScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final sharedNotes = ref.watch(Store().sharedNotesProvider); + final screen = BuildMedia(context).size; + final sharedNotes = ref.watch(sharedNotesProvider); return Column( children: [ const Padding( @@ -26,26 +34,255 @@ class _SharedNotesScreenState extends ConsumerState { textAlign: TextAlign.center, ), ), - if (sharedNotes.isEmpty) const Text('Add notes to get started:'), + if (sharedNotes.isEmpty) ...[ + const Text('Add notes to get started:'), + ], if (sharedNotes.isNotEmpty) - GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - ), - itemBuilder: (BuildContext context, int index) { - final note = sharedNotes[index]; - return SizedBox( - width: BuildMedia(context).width * 0.4, - child: Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(note.content), + SizedBox( + height: screen.height * 0.65, + child: GridView.builder( + itemCount: sharedNotes.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemBuilder: (BuildContext context, int index) { + final note = sharedNotes[index]; + final colorBrightness = note.color == null + ? Brightness.light + : getBrightness(note.color!); + final textStyle = TextStyle( + fontSize: 16.0, + color: colorBrightness == Brightness.light + ? Colors.black54 + : Colors.white70, + ); + return Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + child: Card( + color: note.color ?? Styles.washedStone, + child: SizedBox( + width: screen.width * 0.4, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + FittedBox( + child: Text(note.title, + style: TextStyle(fontSize: 20))), + Flexible( + child: Text( + note.content.length > 80 + ? "${note.content.substring(0, 75)}..." + : note.content, + style: textStyle, + ), + ), + ], + ), + ), + ), + ), + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + backgroundColor: Colors.transparent, + context: context, + builder: (BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).viewInsets.bottom), + child: NoteBottomSheet( + index: index, + ), + ); + }); + }, ), - ), - ); - }, + ); + }, + ), ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 12.0), + child: UiButton( + text: 'Add Note', + onPressed: () { + showModalBottomSheet( + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + backgroundColor: Colors.transparent, + context: context, + builder: (BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom), + child: const NoteBottomSheet( + index: null, + ), + ); + }); + }), + ), ], ); } } + +class NoteBottomSheet extends ConsumerStatefulWidget { + const NoteBottomSheet({super.key, required this.index}); + + final int? index; + + @override + ConsumerState createState() => _NoteBottomSheetState(); +} + +class _NoteBottomSheetState extends ConsumerState { + late final SharedNote? oldNote; + late SharedNote newNote; + + final titleFocusNode = FocusNode(); + final contentFocusNode = FocusNode(); + late final TextEditingController titleController; + late final TextEditingController contentController; + + @override + void initState() { + super.initState(); + if (widget.index == null) { + oldNote = null; + final family = ref.read(familyProvider); + final user = ref.read(userProvider); + newNote = SharedNote( + familyId: family!.id, + createdByUserId: user!.id!, + title: 'Title', + content: '', + tagIds: [], + color: Styles.washedStone, + isMarkdown: false, + ); + } else { + oldNote = ref.read(sharedNotesProvider).elementAt(widget.index!); + newNote = SharedNote.copy(oldNote!); + } + titleController = TextEditingController(text: newNote.title); + contentController = TextEditingController(text: newNote.content); + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) => titleFocusNode.requestFocus(), + ); + } + + @override + Widget build(BuildContext context) { + final screen = BuildMedia(context).size; + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + return Container( + width: screen.width, + decoration: BoxDecoration( + color: newNote.color ?? Styles.washedStone, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0)), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + spreadRadius: 5.0, + blurRadius: 2.0, + offset: Offset(0, -2)) + ]), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + style: const TextStyle(fontSize: 18.0), + maxLines: 1, + focusNode: titleFocusNode, + textAlign: TextAlign.left, + controller: titleController, + // decoration: Styles.inputLavenderBubble(), + ), + TextField( + style: const TextStyle(fontSize: 18.0), + maxLines: 6, + focusNode: contentFocusNode, + textAlign: TextAlign.left, + controller: contentController, + // decoration: Styles.inputLavenderBubble(), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 28.0, horizontal: 36.0), + child: UiButton( + onPressed: !noteChanged(oldNote, newNote) + ? null + : () async { + newNote.content = contentController.text; + newNote.title = titleController.text; + try { + Response? res; + if (widget.index == null) { + res = await ref + .read(apiProvider) + .post('shared_notes', data: newNote.toJson()); + } else { + res = await ref + .read(apiProvider) + .put('shared_notes', data: newNote.toJson()); + } + if (res.data != null && res.data['success']) { + if (widget.index == null) { + ref + .read(dashboardProvider.notifier) + .add({'shared_notes': res.data}); + } else { + ref + .read(dashboardProvider.notifier) + .update({'shared_notes': res.data}); + } + showSnack( + ref: ref, + text: 'Added note', + type: SnackType.success); + } else { + showSnack( + ref: ref, + text: res.data['message'] ?? + 'Unexpected error occurred', + type: SnackType.error); + } + } catch (err) { + showSnack( + ref: ref, + text: 'Unexpected error occurred', + type: SnackType.error); + printRed(err); + } + Navigator.pop(context); + }, + text: 'Save', + ), + ), + ], + ), + ), + ); + } + + bool noteChanged(SharedNote? n, SharedNote m) { + if (n == null) return true; + if (n.content != m.content) return true; + if (n.title != m.content) return true; + if (n.tagIds != m.tagIds) return true; + if (n.color != m.color) return true; + return false; + } +} diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart index c42ce36..4a1ff5d 100644 --- a/lib/features/settings/screens/settings_screen.dart +++ b/lib/features/settings/screens/settings_screen.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../global/api.dart'; +import '../../../global/store.dart'; +import '../../../global/widgets/ui_button.dart'; + class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @@ -11,9 +15,29 @@ class SettingsScreen extends ConsumerStatefulWidget { class _SettingsScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return const Column( + final user = ref.read(userProvider); + final family = ref.read(familyProvider); + return Column( children: [ - Text('Settings'), + const Text('Settings'), + const SizedBox(height: 20), + Text(user!.name), + Text("Username: ${user.username ?? 'N/A'}"), + Text("Email: ${user.email ?? 'N/A'}"), + Text("Family Code: ${family?.code ?? 'N/A'}"), + const Spacer(), + UiButton( + onPressed: () { + ref.read(jwtProvider.notifier).revokeToken(); + // Navigator.pushAndRemoveUntil( + // context, + // MaterialPageRoute(builder: (ctx) => const AccountCreateScreen()), + // (_) => false, + // ); + }, + text: 'Sign Out', + ), + const SizedBox(height: 20), ], ); } diff --git a/lib/global/api.dart b/lib/global/api.dart index c4e2878..0b6a40d 100644 --- a/lib/global/api.dart +++ b/lib/global/api.dart @@ -2,58 +2,104 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/print.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:rluv/global/store.dart'; -class Api { - static final Api _instance = Api._internal(); +import '../models/token.dart'; - factory Api() { - if (_instance.initDone) return _instance; - - _instance.initDone = true; - _instance.dio = Dio(); - _instance.dio.options.baseUrl = "http://localhost:8081/"; - // _instance.dio.options.baseUrl = "https://rluv.fosscat.com/"; - _instance.dio.interceptors.add( - LoggingInterceptor(), - // InterceptorsWrapper( - // onRequest: (RequestOptions options, RequestInterceptorHandler handler) { - // // Do something before request is sent. - // // If you want to resolve the request with custom data, - // // you can resolve a `Response` using `handler.resolve(response)`. - // // If you want to reject the request with a error message, - // // you can reject with a `DioException` using `handler.reject(dioError)`. - // return handler.next(options); - // }, - // onResponse: (Response response, ResponseInterceptorHandler handler) { - // if (response.statusCode != null && - // response.statusCode! < 500 && - // response.statusCode! >= 400) { - // return handler.reject(DioException.badResponse( - // requestOptions: RequestOptions(), - // response: response, - // statusCode: response.statusCode!)); - // } - // // Do something with response data. - // // If you want to reject the request with a error message, - // // you can reject a `DioException` object using `handler.reject(dioError)`. - // return handler.next(response); - // }, - // onError: (DioException e, ErrorInterceptorHandler handler) { - // printPink(e); - // // Do something with response error. - // // If you want to resolve the request with some custom data, - // // you can resolve a `Response` object using `handler.resolve(response)`. - // return handler.next(e); - // }, - // ), - ); - return _instance; +final tokenProvider = StateProvider((ref) { + final jwt = ref.watch(jwtProvider); + printLime('Current token: $jwt'); + if (jwt == null) return null; + try { + return Token.fromJson(JwtDecoder.decode(jwt)); + } catch (_) { + return null; } - Api._internal(); +}); - bool initDone = false; - late final Dio dio; +final jwtProvider = StateNotifierProvider<_JwtNotifier, String?>((ref) { + final prefs = ref.watch(prefsProvider); + final jwt = prefs?.getString('jwt'); + return _JwtNotifier(ref, jwt); +}); + +class _JwtNotifier extends StateNotifier { + _JwtNotifier(this.ref, String? jwt) : super(null) { + if (jwt != null) { + setToken(jwt); + } + } + + final StateNotifierProviderRef ref; + + void setToken(String jwt) { + state = jwt; + printCyan('Loaded jwt into client: $jwt'); + ref.read(prefsProvider)?.setString('jwt', jwt); + } + + void revokeToken() { + printCyan('jwt token revoked'); + state = null; + ref.read(prefsProvider)?.remove('jwt'); + } +} + +final apiProvider = StateNotifierProvider<_ApiNotifier, Dio>((ref) { + return _ApiNotifier(ref, Dio()); +}); + +class _ApiNotifier extends StateNotifier { + _ApiNotifier(this.ref, this.dio) : super(dio) { + // dio.options.baseUrl = "https://fe7d-136-36-2-234.ngrok-free.app"; + // dio.options.baseUrl = "http://localhost:8081/"; + dio.options.baseUrl = "https://rluv.fosscat.com/"; + dio.interceptors.addAll([ + InterceptorsWrapper(onRequest: + (RequestOptions options, RequestInterceptorHandler handler) { + final jwt = ref.read(jwtProvider); + if (jwt != null) { + options.headers['token'] = jwt; + } + return handler.next(options); + }, onResponse: (Response response, ResponseInterceptorHandler handler) { + if (response.statusCode != null) { + if (response.statusCode == 200) { + try { + if ((response.data as Map) + .containsKey('success')) { + if (!response.data['success']) return handler.next(response); + } + if ((response.data as Map) + .containsKey('token')) { + final jwt = response.data['token']; + if (jwt != null) { + ref.read(jwtProvider.notifier).setToken(jwt); + // ref.read(tokenProvider.notifier).state = + // Token.fromJson(jwtData); + } + } + } catch (err) { + printRed('Error in interceptor for token: $err'); + return handler.next(response); + } + } + } + return handler.next(response); + }, onError: (DioException err, ErrorInterceptorHandler handler) { + if (err.response?.statusCode == 403) { + ref.read(jwtProvider.notifier).revokeToken(); + } + }), + _LoggingInterceptor(), + ]); + } + + final Dio dio; + final StateNotifierProviderRef ref; Future?> get(String path) async { try { @@ -64,7 +110,6 @@ class Api { } return null; } catch (err) { - printRed('Error in get: $err'); return null; } } @@ -79,7 +124,6 @@ class Api { } return null; } catch (err) { - printRed('Error in put: $err'); return null; } } @@ -94,7 +138,6 @@ class Api { } return null; } catch (err) { - printRed('Error in put: $err'); return null; } } @@ -109,14 +152,13 @@ class Api { } return null; } catch (err) { - printRed('Error in delete: $err'); return null; } } } -class LoggingInterceptor extends Interceptor { - LoggingInterceptor(); +class _LoggingInterceptor extends Interceptor { + _LoggingInterceptor(); @override Future onRequest( @@ -134,7 +176,7 @@ class LoggingInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) { - logPrint('///*** ERROR RESPONSE ***\\\\\\'); + printRed('///*** ERROR RESPONSE ***\\\\\\'); logPrint('URI: ${err.requestOptions.uri}'); if (err.response != null) { logPrint('STATUS CODE: ${err.response?.statusCode?.toString()}'); @@ -167,15 +209,20 @@ class LoggingInterceptor extends Interceptor { } } - void printJson(Map? s) { - if (kDebugMode) { - if (s == null) { - printAmber({}); - return; + void printJson(dynamic s) { + try { + final data = (s as Map?); + if (kDebugMode) { + if (data == null) { + printAmber({}); + return; + } + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + String prettyprint = encoder.convert(s); + printAmber(prettyprint); } - JsonEncoder encoder = const JsonEncoder.withIndent(' '); - String prettyprint = encoder.convert(s); - printAmber(prettyprint); + } catch (_) { + printAmber(s); } } diff --git a/lib/global/store.dart b/lib/global/store.dart index 99de3e2..5f84de1 100644 --- a/lib/global/store.dart +++ b/lib/global/store.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/print.dart'; import 'package:rluv/global/api.dart'; @@ -12,116 +13,120 @@ import '../models/shared_note.dart'; import '../models/transaction_model.dart'; import '../models/user.dart'; -class Store { - static final Store _instance = Store._internal(); - bool _initDone = false; - SharedPreferences? prefs; +StateProvider prefsProvider = + StateProvider((ref) => null); - factory Store() { - if (_instance._initDone) { - return _instance; - } - _instance._initDone = true; - SharedPreferences.getInstance().then( - (value) => _instance.prefs = value, - ); - _instance.dashboardProvider = - StateNotifierProvider?>( - (ref) { - final family = ref.watch(_instance.familyProvider).valueOrNull; - return DashBoardStateNotifier(family); - }, - ); - // _instance.dashboardProvider = FutureProvider?>( - // (ref) async { - // final family = await ref.watch(_instance.familyProvider.future); - // return Api().get("dashboard/${family.id}"); - // }, - // ); - _instance.budgetProvider = Provider( - (ref) { - final dash = ref.watch(_instance.dashboardProvider); - if (dash == null) return null; - final budgetData = dash['budget'] as Map?; - if (budgetData == null) return null; - return Budget.fromJson(budgetData); - }, - ); - _instance.budgetCategoriesProvider = Provider>((ref) { - final dash = ref.watch(_instance.dashboardProvider); - if (dash == null) return []; - final categoriesData = dash['budget_categories'] as List; - final categories = categoriesData - .map( - (e) => BudgetCategory.fromJson(e as Map), - ) - .toList(); - if (_instance.prefs != null) { - final budgetJson = jsonEncode({'budget_categories': categoriesData}); - printBlue('updated prefs stored categories'); - _instance.prefs!.setString('budget_categories', budgetJson); - } - return categories; - }); - _instance.transactionsProvider = Provider>((ref) { - final dash = ref.watch(_instance.dashboardProvider); - if (dash == null) return []; - final transactions = dash['transactions'] as List; - return transactions - .map( - (e) => Transaction.fromJson(e as Map), - ) - .toList(); - }); - return _instance; - } +// final StateProvider tokenProvider = StateProvider((ref) { +// // final tokenStr = prefs.getString('jwt'); +// // if (tokenStr != null) { +// // return Token.fromJson(jsonDecode(tokenStr)); +// // } +// return null; +// }); - Store._internal(); +final Provider userProvider = Provider( + (ref) { + final dash = ref.watch(dashboardProvider); + if (dash == null) return null; + final userData = dash['user'] as Map; + return User.fromJson(userData); + }, +); - final FutureProvider userProvider = FutureProvider( - (ref) { - return User( - id: 0, - budgetId: 1, - createdAt: DateTime.now(), - familyId: 1, - lastActivityAt: DateTime.now(), - name: 'TEMP', - updatedAt: DateTime.now(), - ); - }, - ); +final Provider familyProvider = Provider( + (ref) { + final dash = ref.watch(dashboardProvider); + if (dash == null) return null; + final familyData = dash['family'] as Map; + return FamilyModel.fromJson(familyData); + }, +); - final FutureProvider familyProvider = - FutureProvider((ref) => FamilyModel( - id: 1, - budgetId: 1, - createdAt: DateTime.now(), - updatedAt: DateTime.now())); +final Provider> budgetCategoriesProvider = + Provider>((ref) { + final dash = ref.watch(dashboardProvider); + if (dash == null) return []; + final categoriesData = dash['budget_categories'] as List; + final categories = categoriesData + .map( + (e) => BudgetCategory.fromJson(e as Map), + ) + .toList(); - late final Provider> budgetCategoriesProvider; - late final Provider budgetProvider; - late final Provider> transactionsProvider; - late final Provider> sharedNotesProvider; - // late final FutureProvider?> dashboardProvider; + final prefs = ref.read(prefsProvider); + final budgetJson = jsonEncode({'budget_categories': categoriesData}); + printBlue('updated prefs stored categories'); + prefs?.setString('budget_categories', budgetJson); - late final StateNotifierProvider?> dashboardProvider; - void fetchDashboard() {} -} + return categories; +}); + +final Provider budgetProvider = Provider( + (ref) { + final dash = ref.watch(dashboardProvider); + if (dash == null) return null; + final budgetData = dash['budget'] as Map?; + if (budgetData == null) return null; + return Budget.fromJson(budgetData); + }, +); + +final Provider> transactionsProvider = + Provider>((ref) { + final dash = ref.watch(dashboardProvider); + if (dash == null) return []; + final transactions = dash['transactions'] as List; + return transactions + .map( + (e) => Transaction.fromJson(e as Map), + ) + .toList(); +}); + +final Provider> sharedNotesProvider = + Provider>( + (ref) { + final dash = ref.watch(dashboardProvider); + if (dash == null) return []; + final sharedNotes = dash['shared_notes'] as List; + return sharedNotes + .map( + (e) => SharedNote.fromJson(e as Map), + ) + .toList(); + }, +); + +final dashboardProvider = + StateNotifierProvider?>( + (ref) { + return DashBoardStateNotifier(ref); + }, +); + +final loadingStateProvider = StateProvider( + (ref) => false, +); class DashBoardStateNotifier extends StateNotifier?> { - DashBoardStateNotifier(FamilyModel? family) : super(null) { - fetchDashboard(family); - } + DashBoardStateNotifier(this.ref) : super(null); - Future fetchDashboard(FamilyModel? family) async { - if (family == null) { - printPink('Unable to get dashboard'); + StateNotifierProviderRef ref; + + Future fetchDashboard() async { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(loadingStateProvider.notifier).state = true, + ); + final token = ref.read(tokenProvider); + if (token?.familyId == null) { + printPink('No token, cannot fetch dashboard'); return; } printAmber('Fetching dashboard'); - state = await Api().get("dashboard/${family.id}"); + state = await ref.read(apiProvider.notifier).get("dashboard"); + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(loadingStateProvider.notifier).state = false, + ); } void update(Map data) { @@ -136,6 +141,7 @@ class DashBoardStateNotifier extends StateNotifier?> { switch (key) { case 'transactions': case 'budget_categories': + case 'shared_notes': final subStateList = state![key] as List; final subStateListObj = subStateList .map( @@ -171,6 +177,7 @@ class DashBoardStateNotifier extends StateNotifier?> { switch (key) { case 'transactions': case 'budget_categories': + case 'shared_noted': final subStateList = state![key] as List; final newState = state; subStateList.add(data.values.first); diff --git a/lib/global/styles.dart b/lib/global/styles.dart index b8cce2c..b160ad6 100644 --- a/lib/global/styles.dart +++ b/lib/global/styles.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; class Styles { // Theme Colors - static const Color purpleNurple = Color(0xffA188A6); + static const Color purpleNurple = Color(0xFFA188A6); + static const Color deepPurpleNurple = Color(0xFF977C9C); static const Color sunflower = Color(0xffFFEE88); static const Color electricBlue = Color(0xFF19647E); static const Color blushingPink = Color(0xFFE78F8E); @@ -10,7 +11,7 @@ class Styles { static const Color emptyBarGrey = Color(0xFFC8C8C8); static const Color lavender = Color(0xFFB8B8FF); static const Color washedStone = Color(0xFFD9D9D9); - + static const Color flounderBlue = Color(0xFFA7E2E3); // Income Colors static const Color incomeBlue = Color(0xFFB8B8FF); static const Color incomeGreen = Color(0xFF0FA102); @@ -43,6 +44,10 @@ class Styles { Color(0xFFFFB563) ]; + // Button Styles + static const Color disabledButton = Color(0xFFA8A8A8); + static const Color disabledButtonText = Color(0xFFD9D9D9); + // Widget Styles static BoxDecoration boxLavenderBubble = BoxDecoration( color: Styles.lavender, diff --git a/lib/global/utils.dart b/lib/global/utils.dart index 341a61d..ad9a723 100644 --- a/lib/global/utils.dart +++ b/lib/global/utils.dart @@ -2,8 +2,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/print.dart'; import 'package:intl/intl.dart'; +import 'package:rluv/global/styles.dart'; +import 'package:rluv/main.dart'; String formatDate(DateTime time) { return DateFormat('EEEE, dd MMMM yyyy').format(time); @@ -70,3 +73,59 @@ void setDevicePortraitOrientation() { DeviceOrientation.portraitDown, ]); } + +enum SnackType { info, success, error } + +void showSnack( + {required WidgetRef ref, + required String text, + SnackType type = SnackType.info, + Duration duration = const Duration(seconds: 2)}) { + final messengerKey = ref.read(scaffoldMessengerKeyProvider); + if (messengerKey.currentState == null) { + printPink('Cannot show snackbar, state == null'); + return; + } + final color = type == SnackType.info + ? Styles.washedStone + : type == SnackType.success + ? Styles.seaweedGreen + : Styles.expensesRed; + final textStyle = TextStyle( + fontSize: 16, + color: color, + ); + messengerKey.currentState!.showSnackBar(SnackBar( + elevation: 8, + backgroundColor: Styles.deepPurpleNurple, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0)), + ), + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 14.0), + child: Icon( + type == SnackType.success ? Icons.check_circle : Icons.info, + color: color), + ), + Text(text, style: textStyle), + ], + ), + ), + )); +} + +bool isEmailValid(String email) { + final RegExp regex = + RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$"); + return regex.hasMatch(email); +} + +bool isUsernameValid(String username) { + final RegExp regex = RegExp(r"[a-zA-Z0-9._-]"); + return regex.hasMatch(username); +} diff --git a/lib/global/widgets/drawer_button.dart b/lib/global/widgets/drawer_button.dart deleted file mode 100644 index ed8dd29..0000000 --- a/lib/global/widgets/drawer_button.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:rluv/global/styles.dart'; - -class CuteDrawerButton extends StatelessWidget { - const CuteDrawerButton( - {super.key, - required this.text, - required this.color, - required this.onPressed}); - - final String text; - final Function onPressed; - final Color color; - final double borderRadius = 12.0; - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - borderRadius: BorderRadius.circular(borderRadius), - onTap: () => onPressed(), - child: Container( - decoration: BoxDecoration( - color: color, - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 2.0, - spreadRadius: 2.0, - offset: Offset(2.5, 2.5), - ) - ], - borderRadius: BorderRadius.circular(borderRadius), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - text, - style: const TextStyle( - fontSize: 14, - color: Styles.washedStone, - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/global/widgets/ui_button.dart b/lib/global/widgets/ui_button.dart new file mode 100644 index 0000000..46e889b --- /dev/null +++ b/lib/global/widgets/ui_button.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:rluv/global/styles.dart'; +import 'package:rluv/global/utils.dart'; + +class UiButton extends StatefulWidget { + const UiButton({ + super.key, + required this.text, + this.color = Styles.deepPurpleNurple, + required this.onPressed, + this.height, + this.width, + this.icon, + this.showLoading = false, + }); + + final String text; + final Function? onPressed; + final Color color; + final double? width; + final double? height; + final Icon? icon; + final bool showLoading; + + @override + State createState() => _UiButtonState(); +} + +class _UiButtonState extends State { + final double borderRadius = 12.0; + bool loading = false; + + @override + Widget build(BuildContext context) { + final computedColor = + widget.onPressed == null ? Styles.disabledButton : widget.color; + final brightness = getBrightness(computedColor); + return SizedBox( + width: widget.width, + height: widget.height, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: widget.onPressed == null || loading + ? null + : widget.showLoading + ? () async { + setState(() => loading = true); + await widget.onPressed!(); + setState(() => loading = false); + } + : () => widget.onPressed!(), + child: Container( + decoration: BoxDecoration( + color: computedColor, + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 2.0, + spreadRadius: 2.0, + offset: Offset(2.5, 2.5), + ) + ], + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: loading + ? [ + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 26, + width: 26, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: brightness == Brightness.dark + ? Styles.lavender + : Styles.electricBlue)), + ), + ] + : [ + if (widget.icon != null) widget.icon!, + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + widget.text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: widget.onPressed == null + ? Styles.disabledButtonText + : brightness == Brightness.dark + ? Styles.washedStone + : Colors.black87, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 7b417de..67bf294 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,34 +1,57 @@ +import 'package:animated_splash_screen/animated_splash_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:rluv/features/account/account_create_screen.dart'; import 'package:rluv/features/budget/screens/budget_overview.dart'; import 'package:rluv/features/notes/screens/notes_screen.dart'; import 'package:rluv/features/settings/screens/settings_screen.dart'; +import 'package:rluv/global/store.dart'; import 'package:rluv/global/styles.dart'; import 'package:rluv/global/utils.dart'; -import 'package:rluv/global/widgets/drawer_button.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -import 'global/store.dart'; +import 'global/api.dart'; +import 'global/widgets/ui_button.dart'; void main() async { - final _ = Store().prefs; - runApp(const MyApp()); + // await Api().loadToken(); + runApp(const ProviderScope(child: MyApp())); } -class MyApp extends StatelessWidget { +class MyApp extends ConsumerStatefulWidget { const MyApp({super.key}); + @override + ConsumerState createState() => _MyAppState(); +} + +class _MyAppState extends ConsumerState { // This widget is the root of your application. @override Widget build(BuildContext context) { - return ProviderScope( - child: MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const Home(), + return MaterialApp( + scaffoldMessengerKey: ref.read(scaffoldMessengerKeyProvider), + debugShowCheckedModeBanner: false, + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: AnimatedSplashScreen.withScreenFunction( + curve: Curves.easeInOutCubic, + splashTransition: SplashTransition.scaleTransition, + splashIconSize: 200, + splash: Image.asset('assets/splash_icon.png'), + backgroundColor: Styles.purpleNurple, + duration: 1, + screenFunction: () async { + final prefs = await SharedPreferences.getInstance(); + ref.read(prefsProvider.notifier).state = prefs; + Widget nextScreen = const AccountCreateScreen(); + if (ref.read(tokenProvider) != null) { + nextScreen = const Home(); + } + return nextScreen; + }, ), ); } @@ -47,11 +70,29 @@ class _HomeState extends ConsumerState { @override void initState() { setDevicePortraitOrientation(); + if (!ref.read(loadingStateProvider)) { + ref.read(dashboardProvider.notifier).fetchDashboard(); + } + WidgetsBinding.instance.addPostFrameCallback( + (_) { + ref.read(currentHomePageProvider.notifier).state = + const BudgetOverviewScreen(initialData: {}); + }, + ); super.initState(); } @override Widget build(BuildContext context) { + if (ref.watch(tokenProvider) == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (ctx) => const AccountCreateScreen()), + (r) => false, + ); + }); + } if (ref.watch(currentHomePageProvider).toString() == "BudgetOverviewScreen") { initData = {}; @@ -65,7 +106,7 @@ class _HomeState extends ConsumerState { child: Column( children: [ SizedBox(height: BuildMedia(context).height * 0.15), - CuteDrawerButton( + UiButton( text: 'Budget', color: drawerColors[4], onPressed: () { @@ -77,7 +118,7 @@ class _HomeState extends ConsumerState { toggleDrawer(); }, ), - CuteDrawerButton( + UiButton( text: 'Notes', color: drawerColors[5], onPressed: () { @@ -90,7 +131,7 @@ class _HomeState extends ConsumerState { }, ), const Spacer(), - CuteDrawerButton( + UiButton( text: 'Settings', color: drawerColors[0], onPressed: () { @@ -132,7 +173,7 @@ final scaffoldKeyProvider = Provider>( ); final currentHomePageProvider = StateProvider( - (ref) => const BudgetOverviewScreen(initialData: {}), + (ref) => Container(), ); final appBarTitleProvider = Provider( @@ -148,3 +189,7 @@ final appBarTitleProvider = Provider( } }, ); + +final scaffoldMessengerKeyProvider = + Provider>( + (ref) => GlobalKey()); diff --git a/lib/models/budget.dart b/lib/models/budget.dart index afeffe9..e0a90ec 100644 --- a/lib/models/budget.dart +++ b/lib/models/budget.dart @@ -8,6 +8,7 @@ part 'budget.g.dart'; class Budget { const Budget({ this.id, + required this.familyId, required this.name, required this.createdAt, required this.updatedAt, @@ -15,6 +16,7 @@ class Budget { }); final int? id; + final int familyId; final String name; @JsonKey(fromJson: boolFromJson, toJson: boolToJson) diff --git a/lib/models/budget.g.dart b/lib/models/budget.g.dart index 3087ba4..204b6bc 100644 --- a/lib/models/budget.g.dart +++ b/lib/models/budget.g.dart @@ -8,6 +8,7 @@ part of 'budget.dart'; Budget _$BudgetFromJson(Map json) => Budget( id: json['id'] as int?, + familyId: json['family_id'] as int, name: json['name'] as String, createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), @@ -16,6 +17,7 @@ Budget _$BudgetFromJson(Map json) => Budget( Map _$BudgetToJson(Budget instance) => { 'id': instance.id, + 'family_id': instance.familyId, 'name': instance.name, 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), diff --git a/lib/models/family_model.dart b/lib/models/family_model.dart index 91720e2..f500f06 100644 --- a/lib/models/family_model.dart +++ b/lib/models/family_model.dart @@ -8,13 +8,14 @@ part 'family_model.g.dart'; class FamilyModel { const FamilyModel({ required this.id, - required this.budgetId, + required this.code, required this.createdAt, required this.updatedAt, this.hide = false, }); - final int id, budgetId; + final int id; + final String? code; @JsonKey(fromJson: boolFromJson, toJson: boolToJson) final bool hide; diff --git a/lib/models/family_model.g.dart b/lib/models/family_model.g.dart index 6c36909..4ad8284 100644 --- a/lib/models/family_model.g.dart +++ b/lib/models/family_model.g.dart @@ -8,7 +8,7 @@ part of 'family_model.dart'; FamilyModel _$FamilyModelFromJson(Map json) => FamilyModel( id: json['id'] as int, - budgetId: json['budget_id'] as int, + code: json['code'] as String?, createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), @@ -17,7 +17,7 @@ FamilyModel _$FamilyModelFromJson(Map json) => FamilyModel( Map _$FamilyModelToJson(FamilyModel instance) => { 'id': instance.id, - 'budget_id': instance.budgetId, + 'code': instance.code, 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), 'updated_at': dateToJson(instance.updatedAt), diff --git a/lib/models/shared_note.dart b/lib/models/shared_note.dart index 6cedf0e..ef8e1ef 100644 --- a/lib/models/shared_note.dart +++ b/lib/models/shared_note.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ui'; import 'package:json_annotation/json_annotation.dart'; @@ -8,27 +9,32 @@ part 'shared_note.g.dart'; @JsonSerializable() class SharedNote { - const SharedNote({ + SharedNote({ this.id, required this.familyId, required this.createdByUserId, required this.content, + required this.title, this.color, this.createdAt, this.updatedAt, + required this.tagIds, this.isMarkdown = false, this.hide = false, }); final int? id; final int familyId, createdByUserId; - final String content; + String content, title; + + @JsonKey(fromJson: _tagIdsFromJson, toJson: _tagIdsToJson) + List tagIds; @JsonKey(fromJson: optionalColorFromJson, toJson: optionalColorToJson) - final Color? color; + Color? color; @JsonKey(fromJson: boolFromJson, toJson: boolToJson) - final bool isMarkdown; + bool isMarkdown; @JsonKey(fromJson: boolFromJson, toJson: boolToJson) final bool hide; @@ -43,4 +49,20 @@ class SharedNote { _$SharedNoteFromJson(json); Map toJson() => _$SharedNoteToJson(this); + + factory SharedNote.copy(SharedNote note) => + SharedNote.fromJson(note.toJson()); +} + +List _tagIdsFromJson(String tagJson) { + final tagsMap = jsonDecode(tagJson) as List; + return tagsMap + .map( + (e) => int.parse(e), + ) + .toList(); +} + +String _tagIdsToJson(List tagIds) { + return jsonEncode(tagIds); } diff --git a/lib/models/shared_note.g.dart b/lib/models/shared_note.g.dart index fb5e067..0aa6034 100644 --- a/lib/models/shared_note.g.dart +++ b/lib/models/shared_note.g.dart @@ -11,9 +11,11 @@ SharedNote _$SharedNoteFromJson(Map json) => SharedNote( familyId: json['family_id'] as int, createdByUserId: json['created_by_user_id'] as int, content: json['content'] as String, + title: json['title'] as String, color: optionalColorFromJson(json['color'] as String?), createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), + tagIds: _tagIdsFromJson(json['tag_ids'] as String), isMarkdown: json['is_markdown'] == null ? false : boolFromJson(json['is_markdown'] as int), @@ -26,6 +28,8 @@ Map _$SharedNoteToJson(SharedNote instance) => 'family_id': instance.familyId, 'created_by_user_id': instance.createdByUserId, 'content': instance.content, + 'title': instance.title, + 'tag_ids': _tagIdsToJson(instance.tagIds), 'color': optionalColorToJson(instance.color), 'is_markdown': boolToJson(instance.isMarkdown), 'hide': boolToJson(instance.hide), diff --git a/lib/models/tag.dart b/lib/models/tag.dart new file mode 100644 index 0000000..614777b --- /dev/null +++ b/lib/models/tag.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'tag.g.dart'; + +enum TagType { + note, +} + +@JsonSerializable() +class Tag { + const Tag({ + this.id, + required this.familyId, + required this.createdByUserId, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + this.hide = false, + }); + + final int? id; + final int familyId, createdByUserId; + final String name; + final TagType type; + + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? createdAt; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? updatedAt; + + factory Tag.fromJson(Map json) => _$TagFromJson(json); + + Map toJson() => _$TagToJson(this); +} diff --git a/lib/models/tag.g.dart b/lib/models/tag.g.dart new file mode 100644 index 0000000..137b880 --- /dev/null +++ b/lib/models/tag.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tag.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Tag _$TagFromJson(Map json) => Tag( + id: json['id'] as int?, + familyId: json['family_id'] as int, + createdByUserId: json['created_by_user_id'] as int, + name: json['name'] as String, + type: $enumDecode(_$TagTypeEnumMap, json['type']), + createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), + ); + +Map _$TagToJson(Tag instance) => { + 'id': instance.id, + 'family_id': instance.familyId, + 'created_by_user_id': instance.createdByUserId, + 'name': instance.name, + 'type': _$TagTypeEnumMap[instance.type]!, + 'hide': boolToJson(instance.hide), + 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), + }; + +const _$TagTypeEnumMap = { + TagType.note: 'note', +}; diff --git a/lib/models/token.dart b/lib/models/token.dart new file mode 100644 index 0000000..be6dee0 --- /dev/null +++ b/lib/models/token.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'token.g.dart'; + +@JsonSerializable() +class Token { + const Token({ + required this.userId, + required this.familyId, + required this.generatedAt, + required this.expiresAt, + }); + + final int familyId, userId; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime generatedAt; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime expiresAt; + + factory Token.fromJson(Map json) => _$TokenFromJson(json); + + Map toJson() => _$TokenToJson(this); +} diff --git a/lib/models/token.g.dart b/lib/models/token.g.dart new file mode 100644 index 0000000..5899882 --- /dev/null +++ b/lib/models/token.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Token _$TokenFromJson(Map json) => Token( + userId: json['user_id'] as int, + familyId: json['family_id'] as int, + generatedAt: dateFromJson(json['generated_at'] as int), + expiresAt: dateFromJson(json['expires_at'] as int), + ); + +Map _$TokenToJson(Token instance) => { + 'family_id': instance.familyId, + 'user_id': instance.userId, + 'generated_at': dateToJson(instance.generatedAt), + 'expires_at': dateToJson(instance.expiresAt), + }; diff --git a/lib/models/user.dart b/lib/models/user.dart index 79eebcb..c0b461b 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -11,6 +11,8 @@ class User { required this.name, required this.familyId, required this.budgetId, + this.email, + this.username, this.createdAt, this.updatedAt, this.lastActivityAt, @@ -20,6 +22,7 @@ class User { final int? id; final int familyId, budgetId; final String name; + final String? username, email; @JsonKey(fromJson: boolFromJson, toJson: boolToJson) final bool hide; diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index cbb3fd8..f82a4d6 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -11,6 +11,8 @@ User _$UserFromJson(Map json) => User( name: json['name'] as String, familyId: json['family_id'] as int, budgetId: json['budget_id'] as int, + email: json['email'] as String?, + username: json['username'] as String?, createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), lastActivityAt: dateFromJson(json['last_activity_at'] as int), @@ -22,6 +24,8 @@ Map _$UserToJson(User instance) => { 'family_id': instance.familyId, 'budget_id': instance.budgetId, 'name': instance.name, + 'username': instance.username, + 'email': instance.email, 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), 'updated_at': dateToJson(instance.updatedAt), diff --git a/lib/splash_screen.dart b/lib/splash_screen.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/splash_screen.dart @@ -0,0 +1 @@ + diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.lock b/pubspec.lock index cb577c8..f9da3d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + animated_splash_screen: + dependency: "direct main" + description: + name: animated_splash_screen + sha256: f45634db6ec4e8cf034c53e03f3bd83898a16fe3c9286bf5510b6831dfcf2124 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" args: dependency: transitive description: @@ -169,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + custom_refresh_indicator: + dependency: "direct main" + description: + name: custom_refresh_indicator + sha256: "65a463f09623f6baf75e45e0c9034e9304810be3f5dfb00a54edde7252f4a524" + url: "https://pub.dev" + source: hosted + version: "2.2.1" dart_style: dependency: transitive description: @@ -222,6 +246,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_lints: dependency: "direct dev" description: @@ -280,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" http_multi_server: dependency: transitive description: @@ -296,6 +336,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + icons_launcher: + dependency: "direct dev" + description: + name: icons_launcher + sha256: af05397792f6d82b93375a8a0253b8db0d3f816ef1dd1bf5c35cbab55321d327 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + image: + dependency: transitive + description: + name: image + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + url: "https://pub.dev" + source: hosted + version: "4.0.17" intl: dependency: "direct main" description: @@ -336,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.2" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -352,6 +416,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd + url: "https://pub.dev" + source: hosted + version: "7.1.1" + markdown_widget: + dependency: "direct main" + description: + name: markdown_widget + sha256: "088feae6be2dd527c7dd54e06ad104a3e70505aff2ce14a3b464482551a0e273" + url: "https://pub.dev" + source: hosted + version: "2.2.0" matcher: dependency: transitive description: @@ -392,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + page_transition: + dependency: transitive + description: + name: page_transition + sha256: a7694bc120b7064a7f57c336914bb8885acf4f70bb3772c30c2fcfe6a85e43ff + url: "https://pub.dev" + source: hosted + version: "2.0.9" path: dependency: transitive description: @@ -424,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.7" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" platform: dependency: transitive description: @@ -440,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" pool: dependency: transitive description: @@ -472,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" shared_preferences: dependency: "direct main" description: @@ -645,6 +757,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + url: "https://pub.dev" + source: hosted + version: "6.1.12" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" + url: "https://pub.dev" + source: hosted + version: "6.0.37" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + url: "https://pub.dev" + source: hosted + version: "2.1.3" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + url: "https://pub.dev" + source: hosted + version: "2.0.18" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + url: "https://pub.dev" + source: hosted + version: "3.0.7" uuid: dependency: "direct main" description: @@ -661,6 +845,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" watcher: dependency: transitive description: @@ -693,6 +885,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" yaml: dependency: transitive description: @@ -702,5 +902,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index d65378c..1e81dc7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,10 @@ dependencies: json_annotation: ^4.8.0 shared_preferences: ^2.1.2 intl: ^0.18.1 + markdown_widget: ^2.2.0 + custom_refresh_indicator: ^2.2.1 + jwt_decoder: ^2.0.1 + animated_splash_screen: ^1.3.0 dev_dependencies: flutter_test: @@ -50,6 +54,7 @@ dev_dependencies: build_runner: ^2.3.3 json_serializable: ^6.6.0 flutter_lints: ^2.0.0 + icons_launcher: ^2.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -63,9 +68,8 @@ flutter: 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 + assets: + - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware @@ -92,3 +96,13 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages +icons_launcher: + image_path: "assets/app_icon.png" + platforms: + android: + enable: true + adaptive_background_color: "#A188A6" + adaptive_foreground_image: "assets/app_icon1.png" + adaptize_round_image: "assets/app_icon_round.png" + ios: + enable: true \ No newline at end of file