diff --git a/android/app/build.gradle b/android/app/build.gradle index e1bf436e..99b62792 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '49' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '3.1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { diff --git a/android/build.gradle b/android/build.gradle index 55c57112..8f31e8ca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -28,4 +15,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..69d1b623 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,21 @@ -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + includeBuild "$flutterSdkPath/packages/flutter_tools/gradle" + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.1.2" apply false + id "org.jetbrains.kotlin.android" version "1.8.10" apply false +} +include ":app" \ No newline at end of file diff --git a/assets/images/icons/google-logo.png b/assets/images/icons/google-logo.png new file mode 100644 index 00000000..001e1a21 Binary files /dev/null and b/assets/images/icons/google-logo.png differ diff --git a/lib/internet/api/api.dart b/lib/internet/api/api.dart index 821ba6e1..f6c27cf6 100644 --- a/lib/internet/api/api.dart +++ b/lib/internet/api/api.dart @@ -9,6 +9,7 @@ import 'package:quimify_client/internet/api/results/inorganic_result.dart'; import 'package:quimify_client/internet/api/results/molecular_mass_result.dart'; import 'package:quimify_client/internet/api/results/organic_result.dart'; import 'package:quimify_client/internet/internet.dart'; +import 'package:quimify_client/internet/api/results/balancer_result.dart'; class Api { static final Api _singleton = Api._internal(); @@ -25,7 +26,7 @@ class Api { static const _apiVersion = 6; static const _clientVersion = 13; - static const _authority = 'api.quimify.com'; + static const _authority = 'api1.quimify.com'; static const _mirrorAuthority = 'api2.quimify.com'; static const _timeout = Duration(seconds: 15); @@ -359,4 +360,28 @@ class Api { return result; } + + Future getBalancedEquation(String equation) async { + BalancerResult? result; + + String? response = await _getBodyWithRetry( + 'balance', + { + 'equation': equation, + }, + ); + + if (response != null) { + try { + result = BalancerResult.fromJson(response, equation); + } catch (error) { + sendError( + context: 'Balancer JSON', + details: error.toString(), + ); + } + } + + return result; + } } diff --git a/lib/internet/api/results/balancer_result.dart b/lib/internet/api/results/balancer_result.dart new file mode 100644 index 00000000..ba3a72f7 --- /dev/null +++ b/lib/internet/api/results/balancer_result.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; + +class BalancerResult { + final String? formula; + final bool present; + final String? originalEquation; + final String? originalReactants; + final String? originalProducts; + final String? balancedEquation; + final String? balancedReactants; + final String? balancedProducts; + final String? error; + + BalancerResult( + this.formula, + this.present, + this.originalEquation, + this.originalReactants, + this.originalProducts, + this.balancedEquation, + this.balancedReactants, + this.balancedProducts, + this.error, + ); + + factory BalancerResult.fromJson(String body, String formula) { + var json = jsonDecode(body); + return BalancerResult( + formula, + json['present'], + json['originalEquation'], + json['originalReactants'], + json['originalProducts'], + json['balancedEquation'], + json['balancedReactants'], + json['balancedProducts'], + json['error'], + ); + } +} diff --git a/lib/internet/api/results/classification.dart b/lib/internet/api/results/classification.dart index f3a76a04..27856dbf 100644 --- a/lib/internet/api/results/classification.dart +++ b/lib/internet/api/results/classification.dart @@ -7,6 +7,7 @@ enum Classification { molecularMassProblem, chemicalProblem, chemicalReaction, + signIn, } const Map stringToClassification = { @@ -18,4 +19,5 @@ const Map stringToClassification = { 'molecularMassProblem': Classification.molecularMassProblem, 'chemicalProblem': Classification.chemicalProblem, 'chemicalReaction': Classification.chemicalReaction, + 'signIn': Classification.signIn, }; diff --git a/lib/internet/api/sign-in/info_google.dart b/lib/internet/api/sign-in/info_google.dart new file mode 100644 index 00000000..27740820 --- /dev/null +++ b/lib/internet/api/sign-in/info_google.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:http/http.dart' as http; + +// Returns Gender and Birthday from Google +Future> getInfoGoogle( + GoogleSignInAccount googleUser) async { + // Retrieve the authentication headers + final Map headers = await googleUser.authHeaders; + + // Safely handle the 'Authorization' header + final String authorizationHeader = headers['Authorization'] ?? ''; + + // Make the initial HTTP GET request to the Google People API + final response = await http.get( + Uri.parse( + 'https://people.googleapis.com/v1/people/me?personFields=genders,birthdays'), // Use your API key as needed + headers: {'Authorization': authorizationHeader}, + ); + + // Handle the response + if (response.statusCode == 200) { + // Decode the JSON response + final data = jsonDecode(response.body); + + // Extract gender information + String gender = 'Gender not found'; + if (data['genders'] != null && data['genders'].isNotEmpty) { + gender = data['genders'][0]['formattedValue']; + } + + // Extract birthday information + String birthday = 'Birthday not found'; + bool yearMissing = false; + + if (data['birthdays'] != null && data['birthdays'].isNotEmpty) { + final birthdayDate = data['birthdays'][0]['date']; + if (birthdayDate != null) { + int? year = birthdayDate['year']; + int? month = birthdayDate['month']; + int? day = birthdayDate['day']; + + if (year != null && month != null && day != null) { + DateTime birthdayDateTime = DateTime(year, month, day); + int timestamp = birthdayDateTime.millisecondsSinceEpoch; + birthday = timestamp.toString(); + } else { + yearMissing = year == null; + birthday = + '${year ?? ''}-${month != null ? month.toString().padLeft(2, '0') : ''}-${day != null ? day.toString().padLeft(2, '0') : ''}'; + } + } + } + + // If the year is missing, make another API call with age range + if (yearMissing) { + final rangedResponse = await http.get( + Uri.parse( + 'https://people.googleapis.com/v1/people/me?personFields=ageRanges'), + headers: {'Authorization': authorizationHeader}, + ); + + if (rangedResponse.statusCode == 200) { + final rangedData = jsonDecode(rangedResponse.body); + if (rangedData['ageRanges'] != null && + rangedData['ageRanges'].isNotEmpty) { + // Extract age range + final ageRange = rangedData['ageRanges'][0]['ageRange']; + if (ageRange != null) { + birthday = ageRange; + } + } + } + } + + // Return both gender and birthday + return {'gender': gender, 'birthday': birthday}; + } else { + // Handle error cases + throw Exception( + 'Failed to fetch gender and birthday info: ${response.statusCode}'); + } +} diff --git a/lib/internet/api/sign-in/userAuthService.dart b/lib/internet/api/sign-in/userAuthService.dart new file mode 100644 index 00000000..9d319162 --- /dev/null +++ b/lib/internet/api/sign-in/userAuthService.dart @@ -0,0 +1,140 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:quimify_client/internet/api/sign-in/info_google.dart'; +import 'package:quimify_client/storage/storage.dart'; + +class UserAuthService { + static final _googleSignIn = GoogleSignIn(scopes: scopes); + static final UserAuthService _instance = UserAuthService(); + static QuimifyIdentity? _user; + + static List scopes = [ + 'email', + 'https://www.googleapis.com/auth/user.birthday.read', + 'https://www.googleapis.com/auth/user.gender.read' + ]; + + // * Constructor, Singleton pattern + initialize() async { + try { + // Sets ISRG Root X1 certificate, not present in Android < 25 + var certificate = await rootBundle.load('assets/ssl/isrg-x1.crt'); + var bytes = certificate.buffer.asUint8List(); + SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); + } catch (_) {} // It's already present in modern devices anyways + } + + // * Getters + + static UserAuthService getInstance() { + return _instance; + } + + static QuimifyIdentity? getUser() { + return _user; + } + + static bool loginRequiered() { + return _instance.hasSkippedLogin() == false && + UserAuthService._user == null; + } + + bool hasSkippedLogin() { + final prefs = Storage(); + return prefs.getBool('userSkippedLogIn') ?? false; + } + + // * Methods + + static Future signOut() async { + if (_googleSignIn.currentUser != null) await _googleSignIn.signOut(); + final prefs = Storage(); + await prefs.saveBool('isAnonymouslySignedIn', false); + UserAuthService._user = null; + return true; + } + + Future signInGoogleUser() async { + final user = await _googleSignIn.signIn(); + if (user == null) return null; + UserAuthService._user = await _signInPOST(user); + return UserAuthService._user; + } + + Future signInAnonymousUser() async { + bool state = await _logInPOST(null); + final prefs = Storage(); + await prefs.saveBool('isAnonymouslySignedIn', state); + QuimifyIdentity identity = QuimifyIdentity(); + UserAuthService._user = identity; + return UserAuthService._user; + } + + Future handleSilentAuthentication() async { + final googleUser = await _googleSignIn.signInSilently(); + if (googleUser != null) { + bool state = await _logInPOST(googleUser); + QuimifyIdentity identity = QuimifyIdentity( + googleUser: googleUser, + photoUrl: googleUser.photoUrl, + displayName: googleUser.displayName ?? 'Quimify', + email: googleUser.email, + ); + return state ? identity : null; + } + return null; + } + + // * Private Class methods + + // TODO: Implement error handling for login requests + // + Future _signInPOST(GoogleSignInAccount googleUser) async { + var data = await getInfoGoogle(googleUser); + QuimifyIdentity identity = QuimifyIdentity( + googleUser: googleUser, + photoUrl: googleUser.photoUrl, + displayName: googleUser.displayName ?? 'Quimify', + email: googleUser.email, + gender: data['gender'], + birthday: data['birthday']); + //format: api.quimify.com/login?id=...&email=...&gender=...&birthday=... + return identity; + } + + // TODO: Logic of hhtp request (make sure to handle errors) + //* Return true if login was successful, false otherwise + Future _logInPOST(GoogleSignInAccount? googleUser) async { + // If user is null, it means that the user is not logged in + if (googleUser == null) return false; + + // _Important_: Do not use this returned Google ID to communicate the + /// currently signed in user to your backend server. Instead, send an ID token + /// which can be securely validated on the server. + var id = googleUser.authentication.then((value) => value.idToken); + //formato api.quimify.com/login?id=...&email=...&gender=...&birthday=... + return true; + } +} + +class QuimifyIdentity { + final GoogleIdentity? googleUser; + final String? photoUrl; + final String? displayName; + final String? gender; + final String? email; + final String? birthday; + + QuimifyIdentity({ + this.googleUser, + this.photoUrl, + this.displayName, + this.email, + this.gender, + this.birthday, + }); +} + +enum AuthProviders { google, none } diff --git a/lib/main.dart b/lib/main.dart index 63ab2eea..24927b1f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,26 +13,21 @@ import 'package:quimify_client/pages/home/home_page.dart'; import 'package:quimify_client/pages/inorganic/nomenclature/nomenclature_page.dart'; import 'package:quimify_client/pages/organic/finding_formula/finding_formula_page.dart'; import 'package:quimify_client/pages/organic/naming/naming_page.dart'; +import 'package:quimify_client/pages/sign-in/sign_in_page.dart'; import 'package:quimify_client/routes.dart'; import 'package:quimify_client/storage/storage.dart'; +import 'internet/api/sign-in/userAuthService.dart'; + main() async { _showLoadingScreen(); + await UserAuthService().initialize(); + Ads().initialize(await Api().getClient()); await Storage().initialize(); - Api().initialize(); - - try { - // Sets ISRG Root X1 certificate, not present in Android < 25 - var certificate = await rootBundle.load('assets/ssl/isrg-x1.crt'); - var bytes = certificate.buffer.asUint8List(); - SecurityContext.defaultContext.setTrustedCertificatesBytes(bytes); - } catch (_) {} // It's already present in modern devices anyways ClientResult? clientResult = await Api().getClient(); - Ads().initialize(clientResult); - runApp( DevicePreview( enabled: false, // !kReleaseMode, @@ -53,12 +48,13 @@ _showLoadingScreen() { _hideLoadingScreen() => FlutterNativeSplash.remove(); class QuimifyApp extends StatelessWidget { - const QuimifyApp({ + QuimifyApp({ Key? key, this.clientResult, }) : super(key: key); final ClientResult? clientResult; + RouteObserver routeObserver = RouteObserver(); @override Widget build(BuildContext context) { @@ -67,12 +63,19 @@ class QuimifyApp extends StatelessWidget { value: const SystemUiOverlayStyle(statusBarColor: Colors.transparent), child: MaterialApp( title: 'Quimify', - home: HomePage(clientResult: clientResult), + home: UserAuthService.loginRequiered() != true + ? HomePage( + clientResult: clientResult, + ) + : SignInPage( + clientResult: clientResult, + ), routes: { Routes.inorganicNomenclature: (context) => const NomenclaturePage(), Routes.organicNaming: (context) => const NamingPage(), Routes.organicFindingFormula: (context) => const FindingFormulaPage(), - Routes.calculatorMolecularMass: (context) => const MolecularMassPage(), + Routes.calculatorMolecularMass: (context) => + const MolecularMassPage(), }, // To get rid of debug banner: debugShowCheckedModeBanner: false, diff --git a/lib/pages/calculator/balancer/balancer_page.dart b/lib/pages/calculator/balancer/balancer_page.dart new file mode 100644 index 00000000..af35c5f3 --- /dev/null +++ b/lib/pages/calculator/balancer/balancer_page.dart @@ -0,0 +1,516 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:quimify_client/internet/ads/ads.dart'; +import 'package:quimify_client/internet/api/api.dart'; +import 'package:quimify_client/internet/api/results/balancer_result.dart'; +import 'package:quimify_client/internet/internet.dart'; +import 'package:quimify_client/pages/calculator/balancer/widget/balancer_products_help_dialog.dart'; +import 'package:quimify_client/pages/calculator/balancer/widget/balancer_reactants_help_dialog.dart'; +import 'package:quimify_client/pages/history/history_entry.dart'; +import 'package:quimify_client/pages/history/history_field.dart'; +import 'package:quimify_client/pages/history/history_page.dart'; +import 'package:quimify_client/pages/widgets/bars/quimify_page_bar.dart'; +import 'package:quimify_client/pages/widgets/dialogs/loading_indicator.dart'; +import 'package:quimify_client/pages/widgets/dialogs/messages/coming_soon_dialog.dart'; +import 'package:quimify_client/pages/widgets/dialogs/messages/message_dialog.dart'; +import 'package:quimify_client/pages/widgets/dialogs/messages/no_internet_dialog.dart'; +import 'package:quimify_client/pages/widgets/objects/help_button.dart'; +import 'package:quimify_client/pages/widgets/objects/history_button.dart'; +import 'package:quimify_client/pages/widgets/objects/quimify_button.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; +import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; +import 'package:quimify_client/storage/history/history.dart'; +import 'package:quimify_client/text.dart'; + +class BalancerPage extends StatefulWidget { + const BalancerPage({Key? key}) : super(key: key); + + @override + State createState() => _BalancerPageState(); +} + +class _BalancerPageState extends State { + final TextEditingController _reactantsController = TextEditingController(); + final TextEditingController _productsController = TextEditingController(); + final FocusNode _reactantsFocusNode = FocusNode(); + final FocusNode _productsFocusNode = FocusNode(); + final ScrollController _scrollController = ScrollController(); + + bool _argumentRead = false; + + String _reactantsLabelText = 'C₆H₁₂O₆ + O₂'; + String _productsLabelText = 'CO₂ + H₂O'; + BalancerResult _result = BalancerResult( + '', + true, + 'C₆H₁₂O₆ + O₂ = CO₂ + H₂O', + 'C₆H₁₂O₆ + O₂', + 'CO₂ + H₂O', + 'C₆H₁₂O₆ + 6O₂ ⟶ 6(CO₂) + 6(H₂O)', + 'C₆H₁₂O₆ + 6O₂', + '6(CO₂) + 6(H₂O)', + null, + ); + + _calculate(String reactants, String products) async { + showLoadingIndicator(context); + String finalEquation; + + //Handling if pressed from history + if (reactants.contains('⟶')){ + List arr1 = reactants.split(' ='); + List arr2 = arr1[0].split('⟶'); + + reactants = arr2[0]; + products = arr2[1]; + + finalEquation = '$reactants = $products'; + } + else { + finalEquation = '$reactants = $products'; // Normal request. Not from history + } + + // Result not found in cache, make an API call + BalancerResult? result = await Api().getBalancedEquation(toDigits(finalEquation)); + + if (result != null) { + if (result.present) { + Ads().showInterstitial(); + + setState(() => _result = result); + + History().saveBalancedEquation(result); // + + // UI/UX actions: + + _reactantsLabelText = reactants; // Sets previous input as label + _productsLabelText = products; // Sets previous input as label + + _reactantsController.clear(); + _productsController.clear(); + + _productsFocusNode.unfocus(); + _reactantsFocusNode.unfocus(); + } else { + if (!mounted) return; // For security reasons + MessageDialog.reportable( + title: 'Sin resultado', + details: result.error != null ? toSubscripts(result.error!) : null, + reportContext: 'Ajustar reacciones', + reportDetails: 'Searched "$finalEquation"', + ).show(context); + } + } else { + if (!mounted) return; // For security reasons + + if (await hasInternetConnection()) { + const MessageDialog( + title: 'Sin resultado', + ).show(context); + } else { + noInternetDialog.show(context); + } + } + hideLoadingIndicator(); + } + + _showHistory() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => HistoryPage( + onStartPressed: () => _reactantsFocusNode.requestFocus(), + entries: History() + .getBalancedEquation() + .map((e) => HistoryEntry( + query: formatBalancer('${e.originalReactants} ⟶ ${e.originalProducts}'), + fields: [ + HistoryField( + 'Reacción', + formatBalancer('${e.originalReactants} ⟶ ${e.originalProducts}'), + ), + HistoryField( + 'Reacción ajustada', + formatBalancer('${e.balancedReactants} ⟶ ${e.balancedProducts}'), + ), + ], + )) + .toList(), + onEntryPressed: (equation) => _calculate(equation, equation), + ), + ), + ); + } + + // Interface: + + void _pressedButton() { + _eraseInitialAndFinalBlanks(); // This may need to be adapted to handle both fields. + + // Check if both fields are empty, even with blanks + bool reactantsEmpty = isEmptyWithBlanks(_reactantsController.text); + bool productsEmpty = isEmptyWithBlanks(_productsController.text); + + if (reactantsEmpty && productsEmpty) { + _reactantsController.clear(); // Clears reactants input + _productsController.clear(); // Clears products input + _reactantsFocusNode.requestFocus(); + } + else if (productsEmpty){ + _productsController.clear(); // Clears products input + _productsFocusNode.requestFocus(); + } else { + _calculate(_reactantsController.text, _productsController.text); + } + } + + _submittedText() { + // Keyboard will be hidden afterwards + _eraseInitialAndFinalBlanks(); + + if (isEmptyWithBlanks(_reactantsController.text)) { + _reactantsController.clear(); // Clears input + } + else if(isEmptyWithBlanks(_productsController.text)){ + _productsController.clear(); // Clears input + } + else { + _calculate(_reactantsController.text, _productsController.text); + } + } + + _tappedOutsideText() { + _reactantsFocusNode.unfocus(); // Hides keyboard + _productsFocusNode.unfocus(); // Hides keyboard + + if (isEmptyWithBlanks(_reactantsController.text)) { + _reactantsController.clear(); // Clears input + } else { + _eraseInitialAndFinalBlanks(); + } + + if (isEmptyWithBlanks(_productsController.text)) { + _productsController.clear(); // Clears input + } else { + _eraseInitialAndFinalBlanks(); + } + } + + _scrollToStart() { + // Goes to the top of the page after a delay: + Future.delayed( + const Duration(milliseconds: 200), + () => WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.minScrollExtent, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ), + ), + ); + } + + _startTypingReactant() { + // Like if the TextField was tapped: + _reactantsFocusNode.requestFocus(); + _scrollToStart(); + } + + _startTypingProduct() { + // Like if the TextField was tapped: + _productsFocusNode.requestFocus(); + _scrollToStart(); + } + + void _eraseInitialAndFinalBlanks() { + _reactantsController.text = noInitialAndFinalBlanks(_reactantsController.text); + _productsController.text = noInitialAndFinalBlanks(_productsController.text); + } + + _pressedShareButton(BuildContext context) => comingSoonDialog.show(context); + + @override + Widget build(BuildContext context) { + const double buttonHeight = 50; + + String? argument = ModalRoute.of(context)?.settings.arguments as String?; + + if (argument != null && !_argumentRead) { + _reactantsFocusNode.requestFocus(); + _argumentRead = true; + } + + return PopScope( + onPopInvoked: (bool didPop) async { + if (!didPop) { + return; + } + + hideLoadingIndicator(); + }, + child: GestureDetector( + onTap: _tappedOutsideText, + child: QuimifyScaffold( + bannerAdName: runtimeType.toString(), + header: const QuimifyPageBar(title: 'Ajustar reacciones'), + body: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(20), + child: Column( + children: [ + GestureDetector( + onTap: _startTypingReactant, // As if the TextField was tapped + child: Container( // Reactants Container + height: 110, + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), + padding: const EdgeInsets.all(20), + alignment: Alignment.topLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Reactivos', + style: TextStyle( + fontSize: 18, + color: QuimifyColors.primary(context), + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + const HelpButton( + dialog: BalancerReactantHelpDialog(), + ), + ], + ), + const Spacer(), + TextField( + autocorrect: false, + enableSuggestions: false, + // Aspect: + cursorColor: QuimifyColors.primary(context), + style: TextStyle( + fontSize: 26, + color: QuimifyColors.primary(context), + fontWeight: FontWeight.bold, + ), + keyboardType: TextInputType.visiblePassword, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 3), + isCollapsed: true, + labelText: _reactantsLabelText, + labelStyle: TextStyle( + color: QuimifyColors.tertiary(context), + fontSize: 26, + fontWeight: FontWeight.bold, + ), + // So hint doesn't go up while typing: + floatingLabelBehavior: FloatingLabelBehavior.never, + // To remove bottom border: + border: const OutlineInputBorder( + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + ), + // Logic: + inputFormatters: [ + FilteringTextInputFormatter.allow( + balancerInputFormatter, + ), + ], + textCapitalization: TextCapitalization.sentences, + scribbleEnabled: false, + focusNode: _reactantsFocusNode, + controller: _reactantsController, + onChanged: (String input) { + _reactantsController.value = _reactantsController.value + .copyWith(text: formatBalancerInput(input)); + }, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _submittedText(), + onTap: _scrollToStart, + ), + ], + ), + ), + ), + + const SizedBox(height: 15), + const Icon(Icons.arrow_downward_sharp, size: 35), + const SizedBox(height: 15), + + GestureDetector( + onTap: _startTypingProduct, // As if the TextField was tapped + child: Container( // Products Container + height: 110, + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), + padding: const EdgeInsets.all(20), + alignment: Alignment.topLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Productos', + style: TextStyle( + fontSize: 18, + color: QuimifyColors.primary(context), + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + const HelpButton( + dialog: BalancerProductHelpDialog(), + ), + ], + ), + const Spacer(), + TextField( + autocorrect: false, + enableSuggestions: false, + // Aspect: + cursorColor: QuimifyColors.primary(context), + style: TextStyle( + fontSize: 26, + color: QuimifyColors.primary(context), + fontWeight: FontWeight.bold, + ), + keyboardType: TextInputType.visiblePassword, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 3), + isCollapsed: true, + labelText: _productsLabelText, + labelStyle: TextStyle( + color: QuimifyColors.tertiary(context), + fontSize: 26, + fontWeight: FontWeight.bold, + ), + // So hint doesn't go up while typing: + floatingLabelBehavior: FloatingLabelBehavior.never, + // To remove bottom border: + border: const OutlineInputBorder( + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + ), + // Logic: + inputFormatters: [ + FilteringTextInputFormatter.allow( + balancerInputFormatter, + ), + ], + textCapitalization: TextCapitalization.sentences, + scribbleEnabled: false, + focusNode: _productsFocusNode, + controller: _productsController, + onChanged: (String input) { + _productsController.value = _productsController.value + .copyWith(text: formatBalancerInput(input)); + }, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _submittedText(), + onTap: _scrollToStart, + ), + ], + ), + ), + ), + + const SizedBox(height: 15), + const Icon(Icons.done_sharp, size: 35), + const SizedBox(height: 15), + + Container( + height: 115, + padding: const EdgeInsets.all(20), + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Reacción ajustada', + style: TextStyle( + fontSize: 18, + color: QuimifyColors.primary(context), + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + IconButton( + color: QuimifyColors.primary(context), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + // To remove padding: + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => _pressedShareButton(context), + icon: const Icon(Icons.share_outlined, size: 26), + ), + ], + ), + const Spacer(), + AutoSizeText( + formatBalancer('${_result.balancedReactants} ⟶ ${_result.balancedProducts!}'), + stepGranularity: 0.1, + maxLines: 1, + style: TextStyle( + fontSize: 26, + color: QuimifyColors.teal(), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Row( + children: [ + HistoryButton( + height: buttonHeight, + onPressed: _showHistory, + ), + const SizedBox(width: 12.5), + Expanded( + child: QuimifyButton.gradient( + height: buttonHeight, + onPressed: _pressedButton, + child: Text( + 'Ajustar', + style: TextStyle( + color: QuimifyColors.inverseText(context), + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/calculator/balancer/widget/balancer_products_help_dialog.dart b/lib/pages/calculator/balancer/widget/balancer_products_help_dialog.dart new file mode 100644 index 00000000..07e590df --- /dev/null +++ b/lib/pages/calculator/balancer/widget/balancer_products_help_dialog.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:quimify_client/pages/widgets/dialogs/help_slides_dialog.dart'; +import 'package:quimify_client/pages/widgets/dialogs/widgets/dialog_content_text.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; + +class BalancerProductHelpDialog extends StatelessWidget { + const BalancerProductHelpDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return HelpSlidesDialog( + titleToContent: { + 'Productos': [ + //Primera Página + const Center( + child: DialogContentText( + richText: 'Los productos es como se le llaman a las sustancias o ' + 'compuestos obtenidas luego de ocurrir una reacción química.' + ' Se situan en el lado derecho de la reacción', + ), + ), + const DialogContentText( + richText: '*Ejemplo:*', + ), + Center( + child: RichText( + text: TextSpan( + style: TextStyle( + color: QuimifyColors.primary(context), + fontSize: 16, + fontFamily: 'CeraPro', + ), + children: const [ + TextSpan( + text: '2H + O', + ), + TextSpan( + text: ' ➔ H₃O', + style: TextStyle(color: Colors.blue), + ), + ], + ), + ), + ), + ], + }, + ); + } +} diff --git a/lib/pages/calculator/balancer/widget/balancer_reactants_help_dialog.dart b/lib/pages/calculator/balancer/widget/balancer_reactants_help_dialog.dart new file mode 100644 index 00000000..42850c66 --- /dev/null +++ b/lib/pages/calculator/balancer/widget/balancer_reactants_help_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:quimify_client/pages/widgets/dialogs/help_slides_dialog.dart'; +import 'package:quimify_client/pages/widgets/dialogs/widgets/dialog_content_text.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; + +class BalancerReactantHelpDialog extends StatelessWidget { + const BalancerReactantHelpDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return HelpSlidesDialog( + titleToContent: { + 'Reactivos': [ + //Primera Página + const Center( + child: DialogContentText( + richText: + 'Los reactivos es como se le llaman a las sustancia o compuesto añadidos a un ' + 'sistema para provocar una reacción química. Se situan en el lado ' + 'izquierdo de la reacción', + ), + ), + const DialogContentText( + richText: '*Ejemplo:*', + ), + Center( + child: RichText( + text: TextSpan( + style: TextStyle( + color: QuimifyColors.primary(context), + fontSize: 16, + fontFamily: 'CeraPro', + ), + children: const [ + TextSpan( + text: '2H + O', + style: TextStyle(color: Colors.blue), + ), + TextSpan(text: ' ➔ H₃O'), + ], + ), + ), + ), + ], + }, + ); + } +} diff --git a/lib/pages/calculator/calculator_page.dart b/lib/pages/calculator/calculator_page.dart index e86d6bf0..d5fcfa41 100644 --- a/lib/pages/calculator/calculator_page.dart +++ b/lib/pages/calculator/calculator_page.dart @@ -1,24 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:quimify_client/pages/calculator/balancer/balancer_page.dart'; import 'package:quimify_client/pages/calculator/molecular_mass/molecular_mass_page.dart'; import 'package:quimify_client/pages/calculator/widgets/calculator_help_dialog.dart'; import 'package:quimify_client/pages/home/widgets/quimify_card.dart'; import 'package:quimify_client/pages/widgets/dialogs/messages/coming_soon_dialog.dart'; import 'package:quimify_client/pages/widgets/objects/quimify_section_title.dart'; -import 'package:quimify_client/pages/widgets/quimify_colors.dart'; class CalculatorPage extends StatelessWidget { const CalculatorPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Column( + return const Column( children: [ - const QuimifySectionTitle( + QuimifySectionTitle( title: 'Masa molecular', helpDialog: CalculatorHelpDialog(), ), - const SizedBox(height: 15), - const QuimifyCard( + SizedBox(height: 15), + QuimifyCard( body: { 'Fe₂O₃': '159.68 g/mol', 'C₅H₆O₂': '110.10 g/mol', @@ -29,21 +29,18 @@ class CalculatorPage extends StatelessWidget { }, page: MolecularMassPage(), ), - const QuimifySectionTitle( + QuimifySectionTitle( title: 'Ajustar reacciones', helpDialog: comingSoonDialog, ), - const SizedBox(height: 15), - QuimifyCard.comingSoon( - comingSoonBody: Text( - '⇄', - style: TextStyle( - height: 0.9, // TODO iOS? - fontSize: 36, - fontWeight: FontWeight.w600, - color: QuimifyColors.teal(), - ), - ), + SizedBox(height: 15), + QuimifyCard( + body: { + '2H + O ⟶ H₃O': '6H + 2O ⟶ 2(H₃O)', + 'Fe₂O₃ + C ⟶ Fe + CO₂': '2(Fe₂O₃) + 3C ⟶ 4Fe + 3(CO₂)', + 'Cl + 2OP ⟶ Cl₂O + P': '4Cl + 2(OP) ⟶ 2(Cl₂O) + 2P', + }, + page: BalancerPage(), ), ], ); diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 2c2b4d5d..358dff27 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -4,6 +4,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:quimify_client/internet/api/results/client_result.dart'; import 'package:quimify_client/pages/calculator/calculator_page.dart'; +import 'package:quimify_client/pages/home/widgets/quimify_avatar.dart'; import 'package:quimify_client/pages/home/widgets/quimify_menu_button.dart'; import 'package:quimify_client/pages/inorganic/inorganic_page.dart'; import 'package:quimify_client/pages/organic/organic_page.dart'; @@ -24,7 +25,7 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends State with RouteAware { final List _pages = const [ InorganicPage(), OrganicPage(), @@ -122,8 +123,8 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return PopScope( - onPopInvoked: (bool didPop) async { - if (!didPop) { + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) { return; } @@ -131,7 +132,7 @@ class _HomePageState extends State { }, child: QuimifyScaffold.noAd( header: SafeArea( - bottom: false, // So it's not inside status bar + bottom: false, child: Container( padding: const EdgeInsets.only( top: 15, // TODO 17.5? @@ -164,6 +165,9 @@ class _HomePageState extends State { height: 17, color: QuimifyColors.inverseText(context), ), + //const SizedBox(width: 120), + const Spacer(), + UserAvatar() ], ), ), diff --git a/lib/pages/home/widgets/quimify_avatar.dart b/lib/pages/home/widgets/quimify_avatar.dart new file mode 100644 index 00000000..0919004a --- /dev/null +++ b/lib/pages/home/widgets/quimify_avatar.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:quimify_client/internet/api/sign-in/userAuthService.dart'; +import 'package:quimify_client/pages/profile/profile_page.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; + +class UserAvatar extends StatefulWidget { + const UserAvatar({Key? key}) : super(key: key); + + @override + State createState() => _UserAvatarState(); +} + +class _UserAvatarState extends State { + QuimifyIdentity? user = UserAuthService.getUser(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 15.0), + child: IconButton( + padding: EdgeInsets.zero, + iconSize: 38, + alignment: Alignment.centerRight, + icon: user?.photoUrl != null + ? CircleAvatar( + backgroundImage: NetworkImage(user?.photoUrl ?? ''), + radius: 19, + ) + : Icon( + Icons.account_circle, + color: QuimifyColors.inverseText(context), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfilePage( + onUserUpdated: updateUser, + user: user, + ), + ), + ); + }, + ), + ); + } + + void updateUser(QuimifyIdentity? newUser) { + // Update the user state, so the page gets updated. + setState(() { + user = newUser; + }); + } +} diff --git a/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart b/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart index 8dc89f9d..59ff7550 100644 --- a/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart +++ b/lib/pages/organic/naming/widgets/radical_factory/radical_factory_dialog.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:quimify_client/pages/organic/naming/widgets/radical_factory/carbons_help_dialog.dart'; import 'package:quimify_client/pages/organic/naming/widgets/radical_factory/tip_shape_help_dialog.dart'; diff --git a/lib/pages/profile/profile_page.dart b/lib/pages/profile/profile_page.dart new file mode 100644 index 00000000..e3c3e50c --- /dev/null +++ b/lib/pages/profile/profile_page.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:quimify_client/internet/api/sign-in/userAuthService.dart'; +import 'package:quimify_client/pages/widgets/quimify_scaffold.dart'; + +import 'package:quimify_client/internet/api/results/client_result.dart'; +import 'package:quimify_client/pages/widgets/bars/quimify_page_bar.dart'; +import 'package:quimify_client/pages/widgets/dialogs/loading_indicator.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; + +class ProfilePage extends StatefulWidget { + final ClientResult? clientResult; + final Function(QuimifyIdentity?) onUserUpdated; + QuimifyIdentity? user; + + ProfilePage( + {Key? key, this.clientResult, required this.onUserUpdated, this.user}) + : super(key: key); + + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + void _updateUserProfile() { + updateUserProfile(UserAuthService.getUser()); + } + + updateUserProfile(QuimifyIdentity? newUser) { + print("UPDATING USER PROFILE"); + + // UPDATE THE PROFILE PAGE + updateUser(newUser); + // REBUILD THE PROFILE PAGE + widget.onUserUpdated(newUser); + } + + @override + Widget build(BuildContext context) { + var defaultLogo = const AssetImage('assets/images/logo.png'); + if (widget.user?.googleUser is GoogleIdentity && widget.user != null) { + return PopScope( + onPopInvokedWithResult: (didPop, dynamic) async { + if (!didPop) { + return; + } + + hideLoadingIndicator(); + }, + child: QuimifyScaffold.noAd( + header: + QuimifyPageBar(title: 'Perfil', onPressed: _updateUserProfile), + body: Container( + width: 900, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: QuimifyColors.foreground(context), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + CircleAvatar( + radius: 50, + backgroundImage: widget.user?.photoUrl != null + ? NetworkImage(widget.user?.photoUrl ?? '') + : defaultLogo, + ), + const SizedBox(height: 20), + Text( + 'Nombre: ${widget.user?.displayName ?? 'No hay nombre disponible'}', + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + Text( + 'Email: ${widget.user?.email}', + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + // TODO: Implement the "Gana dinero con Quimify" functionality here + }, + child: const Text('Gana dinero con Quimify'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + await UserAuthService.signOut(); + + updateUserProfile(UserAuthService.getUser()); + }, + child: const Text('Cerrar Sesión'), + ), + ], + ), + ), + ), + ); + } + return PopScope( + onPopInvokedWithResult: (bool didPop, dynamic) async { + if (!didPop) { + return; + } + + hideLoadingIndicator(); + }, + child: QuimifyScaffold.noAd( + header: const QuimifyPageBar(title: 'Perfil'), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 50), + ElevatedButton.icon( + onPressed: () async { + final user = + await UserAuthService().signInGoogleUser(); + if (user == null) return; + _updateUserProfile(); + }, + icon: Image.asset( + 'assets/images/icons/google-logo.png', + height: 24, + ), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + const SizedBox(height: 50), + ], + ), + ); + }, + ), + ), + ), + ), + ); + } + + void updateUser(QuimifyIdentity? newUser) { + setState(() { + widget.user = newUser; + }); + } +} diff --git a/lib/pages/sign-in/sign_in_page.dart b/lib/pages/sign-in/sign_in_page.dart new file mode 100644 index 00000000..e6f63ada --- /dev/null +++ b/lib/pages/sign-in/sign_in_page.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:quimify_client/pages/home/home_page.dart'; + +import 'package:quimify_client/internet/api/results/client_result.dart'; +import 'package:quimify_client/internet/api/sign-in/userAuthService.dart'; +import 'package:quimify_client/pages/widgets/quimify_colors.dart'; + +class SignInPage extends StatelessWidget { + final ClientResult? clientResult; + + static UserAuthService userAuthService = UserAuthService(); + + const SignInPage({ + Key? key, + this.clientResult, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: QuimifyColors.background(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: constraints.maxHeight * 0.3, + maxWidth: constraints.maxWidth * 0.8, + ), + child: Image.asset( + 'assets/images/logo.png', + height: 175, // Set a fixed height for the image + ), + ), + const SizedBox(height: 50), + + // Google Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = await UserAuthService().signInGoogleUser(); + if (user == null) return; + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage(clientResult: clientResult))); + }, + icon: Image.asset('assets/images/icons/google-logo.png', + height: 24), + label: const Text('Iniciar Sesión con Google'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + + const SizedBox(height: 20), + + // Anonymous Sign-In button + ElevatedButton.icon( + onPressed: () async { + final user = + await UserAuthService().signInAnonymousUser(); + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => + HomePage(clientResult: clientResult))); + }, + icon: const Icon(Icons.person_off_outlined, + color: Colors.grey, size: 24), + label: const Text('Saltar'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + + const SizedBox(height: 50), + + Image.asset( + 'assets/images/branding.png', + height: 25, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/widgets/bars/quimify_page_bar.dart b/lib/pages/widgets/bars/quimify_page_bar.dart index cafb319f..c9ec7d2d 100644 --- a/lib/pages/widgets/bars/quimify_page_bar.dart +++ b/lib/pages/widgets/bars/quimify_page_bar.dart @@ -7,8 +7,10 @@ class QuimifyPageBar extends StatelessWidget { const QuimifyPageBar({ Key? key, required this.title, + this.onPressed, }) : super(key: key); + final VoidCallback? onPressed; // Add this optional parameter final String title; @override @@ -27,7 +29,10 @@ class QuimifyPageBar extends StatelessWidget { QuimifyIconButton.square( height: 50, backgroundColor: QuimifyColors.secondaryTeal(context), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => { + onPressed?.call(), + Navigator.of(context).pop(), + }, icon: Icon( Icons.arrow_back, color: QuimifyColors.inverseText(context), diff --git a/lib/routes.dart b/lib/routes.dart index 1937eeda..62b71ce9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,6 +5,7 @@ class Routes { static const String organicNaming = 'organic/naming'; static const String organicFindingFormula = 'organic/finding-formula'; static const String calculatorMolecularMass = 'calculator/molecular-mass'; + static const String signIn = 'sign-in'; static final Map fromClassification = { Classification.inorganicFormula: inorganicNomenclature, @@ -12,6 +13,7 @@ class Routes { Classification.inorganicName: inorganicNomenclature, Classification.organicName: organicFindingFormula, Classification.molecularMassProblem: calculatorMolecularMass, + Classification.signIn: signIn, }; static bool contains(Classification classification) => diff --git a/lib/storage/history/history.dart b/lib/storage/history/history.dart index debe3651..deb7db62 100644 --- a/lib/storage/history/history.dart +++ b/lib/storage/history/history.dart @@ -1,8 +1,10 @@ import 'dart:convert'; +import 'package:quimify_client/internet/api/results/balancer_result.dart'; import 'package:quimify_client/internet/api/results/inorganic_result.dart'; import 'package:quimify_client/internet/api/results/molecular_mass_result.dart'; import 'package:quimify_client/internet/api/results/organic_result.dart'; +import 'package:quimify_client/storage/history/results/balancer_local_result.dart'; import 'package:quimify_client/storage/history/results/inorganic_local_result.dart'; import 'package:quimify_client/storage/history/results/molecular_mass_local_result.dart'; import 'package:quimify_client/storage/history/results/organic_formula_local_result.dart'; @@ -22,6 +24,7 @@ class History { static const String _organicFormulasKey = 'organic-formulas'; static const String _organicNamesKey = 'organic-names'; static const String _molecularMassesKey = 'molecular-masses'; + static const String _balancedEquationKey = 'balanced-equations'; // Private: @@ -85,4 +88,14 @@ class History { MolecularMassLocalResult.fromResult(result), getMolecularMasses(), ); + + List getBalancedEquation() => + _fetch(_balancedEquationKey, BalancerLocalResult.fromJson) + .cast(); + + saveBalancedEquation(BalancerResult result) => _save( + _balancedEquationKey, + BalancerLocalResult.fromResult(result), + getBalancedEquation(), + ); } diff --git a/lib/storage/history/results/balancer_local_result.dart b/lib/storage/history/results/balancer_local_result.dart new file mode 100644 index 00000000..7a716bf1 --- /dev/null +++ b/lib/storage/history/results/balancer_local_result.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:quimify_client/internet/api/results/balancer_result.dart'; + +class BalancerLocalResult { + late final String originalReactants; + late final String originalProducts; + late final String balancedReactants; + late final String balancedProducts; + + BalancerLocalResult( + this.originalReactants, + this.originalProducts, + this.balancedReactants, + this.balancedProducts, + ); + + factory BalancerLocalResult.fromResult(BalancerResult result) => + BalancerLocalResult( + result.originalReactants!, + result.originalProducts!, + result.balancedReactants!, + result.balancedProducts!, + ); + + factory BalancerLocalResult.fromJson(String body) { + var json = jsonDecode(body); + return BalancerLocalResult( + json['originalReactants'], + json['originalProducts'], + json['balancedReactants'], + json['balancedProducts'], + ); + } + + String toJson() => jsonEncode({ + 'originalReactants': originalReactants, + 'originalProducts': originalProducts, + 'balancedReactants': balancedReactants, + 'balancedProducts': balancedProducts, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BalancerLocalResult && + originalReactants == other.originalReactants && + originalProducts == other.originalProducts && + balancedReactants == other.balancedReactants && + balancedProducts == other.balancedProducts; + + @override + int get hashCode => + originalReactants.hashCode ^ + originalProducts.hashCode ^ + balancedReactants.hashCode ^ + balancedProducts.hashCode; +} diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 5c3ca3ff..f50de25a 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -17,4 +17,8 @@ class Storage { String? get(String key) => sharedPreferences.getString(key); save(String key, String value) => sharedPreferences.setString(key, value); + + saveBool(String key, bool value) => sharedPreferences.setBool(key, value); + + getBool(String key) => sharedPreferences.getBool(key); } diff --git a/lib/text.dart b/lib/text.dart index aa95b741..b1786a64 100644 --- a/lib/text.dart +++ b/lib/text.dart @@ -7,6 +7,9 @@ RegExp inputFormatter = RegExp(r'[A-Za-zÁ-ú\d \(\),\-+' RegExp formulaInputFormatter = RegExp(r'[A-IK-PR-Za-ik-pr-z\d\(\)' r'\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089]'); +RegExp balancerInputFormatter = RegExp(r'[A-IK-PR-Za-ik-pr-z\d\(\)' + r'\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089\s\+]'); + const Map digitToSubscript = { '0': '\u2080', '1': '\u2081', @@ -31,6 +34,38 @@ String toSubscripts(String input) { return result; } +String toSubscriptsIfNotCoefficient(String input) { + String result = ''; + bool isCoefficient = true; // Assume first number sequence is a coefficient + bool inNumber = false; // Flag to track when we are within a number + + for (int i = 0; i < input.length; i++) { + String char = input[i]; + if (char == ' ' || char == '+') { + isCoefficient = true; // Reset for possible next coefficient + result += char; + } else if (RegExp(r'[0-9]').hasMatch(char)) { + if (isCoefficient) { + // If starting a number sequence after a space or at the start + if (!inNumber) { + inNumber = true; + result += char; // Append the digit normally + } else { + result += char; // Continue appending digits of a coefficient + } + } else { + result += digitToSubscript[char]!; // Convert to subscript if not coefficient + } + } else { + inNumber = false; // No longer in a number, reset flag + isCoefficient = false; // Any non-space non-digit marks end of a coefficient + result += char; + } + } + + return result; +} + bool isDigit(String char) => digitToSubscript.containsKey(char) || digitToSubscript.containsValue(char); @@ -84,6 +119,22 @@ String toCapsAfterNotAnUppercaseLetter(String input) { return result; } +String toCapsAfterSpaceOrPlusSign(String input) { + if (input.isEmpty) return input; + + String result = input[0]; + + for (int i = 1; i < input.length; i++) { + if (input[i - 1] == ' ' || input[i - 1] == '+') { + result += input[i].toUpperCase(); + } else { + result += input[i]; + } + } + + return result; +} + String toDigits(String input) { String result = ''; @@ -140,3 +191,13 @@ String formatStructureInput(String structure) => String formatStructure(String structure) => toSpacedBonds(toCapsAfterNotAnUppercaseLetter( toSubscripts(toCapsAfterDigitOrParentheses((capFirst(structure)))))); + +String formatBalancer(String equation) => + toSubscriptsIfNotCoefficient(equation); + +String formatBalancerInput(String equation) => + capFirst( + toCapsAfterSpaceOrPlusSign( + toCapsAfterNotAnUppercaseLetter( + toCapsAfterDigitOrParentheses( + toSubscriptsIfNotCoefficient(equation))))); diff --git a/pubspec.lock b/pubspec.lock index 1a630a7a..97186187 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: archive - sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.3.9" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -77,34 +77,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.2" built_collection: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.2" + version: "8.9.2" characters: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -173,10 +173,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -229,18 +229,18 @@ packages: dependency: "direct dev" description: name: dependency_validator - sha256: "08349175533ed0bd06eb9b6043cde66c45b2bfc7ebc222a7542cdb1324f1bf03" + sha256: f727a5627aa405965fab4aef4f468e50a9b632ba0737fd2f98c932fec6d712b9 url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" device_frame: dependency: transitive description: name: device_frame - sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d + sha256: d031a06f5d6f4750009672db98a5aa1536aa4a231713852469ce394779a23d75 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" device_info_plus: dependency: "direct main" description: @@ -253,26 +253,26 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" device_preview: dependency: "direct main" description: name: device_preview - sha256: "2f097bf31b929e15e6756dbe0ec1bcb63952ab9ed51c25dc5a2c722d2b21fdaf" + sha256: a694acdd3894b4c7d600f4ee413afc4ff917f76026b97ab06575fe886429ef19 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" diacritic: dependency: "direct main" description: name: diacritic - sha256: a84e03ec2779375fb86430dbe9d8fba62c68376f2499097a5f6e75556babe706 + sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" envied: dependency: "direct main" description: @@ -317,18 +317,18 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -431,10 +431,10 @@ packages: dependency: transitive description: name: flutter_spinkit - sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: e2070dea278f09ae30885872138ccae75292b33b7af2c241fec5ceafd980c374 + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -457,18 +457,18 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -477,22 +477,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + url: "https://pub.dev" + source: hosted + version: "0.3.1+4" google_mobile_ads: dependency: "direct main" description: name: google_mobile_ads - sha256: d2ef5ec1e1f31137fc241bdeab3037c31062d387dd221fd884fb1160444c788b + sha256: e2d18992d30b2be77cb6976b931112fc3c4612feffb5eb7a8b036bd7a64934da url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.1.0" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "0608de03fc541ece4f91ba3e01a68b17cce7a6cf42bd59e40bbe5c55cc3a49d8" + url: "https://pub.dev" + source: hosted + version: "6.1.30" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "4898410f55440049e1ba8f15411612d9f89299d89c61cd9baf7e02d56ff81ac7" + url: "https://pub.dev" + source: hosted + version: "5.7.7" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "042805a21127a85b0dc46bba98a37926f17d2439720e8a459d27045d8ef68055" + url: "https://pub.dev" + source: hosted + version: "0.12.4+2" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: @@ -537,10 +585,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -561,10 +609,34 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -593,34 +665,34 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" nested: dependency: transitive description: @@ -649,10 +721,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider_linux: dependency: transitive description: @@ -665,26 +737,26 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: a9346a3fbba7546a28374bdbcd7f54ea48bb47772bf3a7ab4bfaadc40bc8b8c6 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "6.0.2" photo_view: dependency: "direct main" description: @@ -697,10 +769,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -713,42 +785,34 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.1+2" pointer_interceptor_ios: dependency: transitive description: name: pointer_interceptor_ios - sha256: "4282ebfe21b54e21e26ab982c6086f0a67dc63423026bfba8db39a2e22045f26" + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" pointer_interceptor_platform_interface: dependency: transitive description: name: pointer_interceptor_platform_interface - sha256: "59a446ead3be360bde72c3725f5ecacbba203c8a760e3061024c20f7da53f825" + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.0+1" pointer_interceptor_web: dependency: transitive description: name: pointer_interceptor_web - sha256: dfd32b9c6e01a18f80535e7791e9d3add009a3395645d6db2f1e22b2642bfab4 + sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" url: "https://pub.dev" source: hosted - version: "0.10.1" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" + version: "0.10.2+1" pool: dependency: transitive description: @@ -761,10 +825,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -777,66 +841,66 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shelf: dependency: transitive description: @@ -849,10 +913,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" signed_spacing_flex: dependency: "direct main" description: @@ -870,18 +934,18 @@ packages: dependency: "direct main" description: name: smooth_page_indicator - sha256: "725bc638d5e79df0c84658e1291449996943f93bacbc2cec49963dbbab48d8ae" + sha256: "3b28b0c545fa67ed9e5997d9f9720d486f54c0c607e056a1094544e36934dff3" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0+3" source_gen: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: @@ -934,10 +998,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -966,66 +1030,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.2" vector_math: dependency: transitive description: @@ -1034,6 +1098,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" watcher: dependency: transitive description: @@ -1046,34 +1118,42 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webview_flutter: dependency: "direct main" description: name: webview_flutter - sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.9.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 + sha256: "6e64fcb1c19d92024da8f33503aaeeda35825d77142c01d0ea2aa32edc79fdc8" url: "https://pub.dev" source: hosted - version: "3.16.0" + version: "3.16.7" webview_flutter_platform_interface: dependency: transitive description: @@ -1086,42 +1166,42 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 + sha256: "1942a12224ab31e9508cf00c0c6347b931b023b8a4f0811e5dec3b06f94f117d" url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "3.15.0" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.5.0" yaml: dependency: transitive description: @@ -1131,5 +1211,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: "3.2.6" - flutter: ">=3.16.6" + dart: "3.5.3" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8cfaf724..314396b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,10 +22,11 @@ dependencies: envied: ^0.3.0+3 signed_spacing_flex: ^1.1.0 device_preview: ^1.1.0 - google_mobile_ads: ^4.0.0 + google_mobile_ads: 5.1.0 fast_rich_text: ^0.0.1 webview_flutter: ^4.7.0 device_info_plus: ^9.1.2 + google_sign_in: ^6.2.1 dev_dependencies: flutter_test: @@ -45,9 +46,12 @@ flutter: - assets/images/dietanoic-acid.png - assets/images/discord.png - assets/images/gmail.png + - assets/images/logo.png + - assets/images/branding.png - assets/images/icons/logo.png - assets/images/icons/branding-slim.png + - assets/images/icons/google-logo.png - assets/images/icons/search.png - assets/images/icons/lock.png @@ -119,7 +123,7 @@ flutter: weight: 100 environment: - sdk: "3.2.6" + sdk: "3.5.3" # Icon: flutter_icons: