├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── GoogleService-Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── firebase_app_id_file.json ├── .gitignore └── Podfile ├── .firebaserc ├── assets ├── members.png ├── no_hive.png ├── hive_work.png ├── discussion.png └── onboarding.gif ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── jerondev │ │ │ │ │ └── studyhive │ │ │ │ │ └── studyhive │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── lib ├── src │ ├── hive │ │ ├── presentation │ │ │ ├── manager │ │ │ │ ├── join_controller.dart │ │ │ │ ├── questions_controller.dart │ │ │ │ ├── discussions_controller.dart │ │ │ │ ├── poll_controller.dart │ │ │ │ ├── hive_controller.dart │ │ │ │ └── create_controller.dart │ │ │ ├── bindings │ │ │ │ ├── join_binding.dart │ │ │ │ ├── create_binding.dart │ │ │ │ └── hive_binding.dart │ │ │ └── pages │ │ │ │ ├── members.dart │ │ │ │ ├── new_material.dart │ │ │ │ ├── new_poll.dart │ │ │ │ ├── create.dart │ │ │ │ ├── new_discussion.dart │ │ │ │ ├── new_question.dart │ │ │ │ └── hive.dart │ │ ├── domain │ │ │ ├── entities │ │ │ │ ├── media.dart │ │ │ │ ├── media.g.dart │ │ │ │ ├── topic.dart │ │ │ │ ├── topic.g.dart │ │ │ │ ├── hive.dart │ │ │ │ ├── hive.g.dart │ │ │ │ ├── message.dart │ │ │ │ ├── message.g.dart │ │ │ │ └── media.freezed.dart │ │ │ ├── use_cases │ │ │ │ ├── details.dart │ │ │ │ ├── create.dart │ │ │ │ ├── list.dart │ │ │ │ └── post_message.dart │ │ │ └── repositories │ │ │ │ └── hive_repository.dart │ │ └── data │ │ │ ├── hive_service.dart │ │ │ ├── local │ │ │ └── data_sources │ │ │ │ └── hive_local_database.dart │ │ │ ├── remote │ │ │ └── data_sources │ │ │ │ └── hive_remote_database.dart │ │ │ └── repositories │ │ │ └── hive_repository_impl.dart │ ├── profile │ │ ├── presentation │ │ │ ├── manager │ │ │ │ ├── profile_controller.dart │ │ │ │ ├── profile_binding.dart │ │ │ │ └── setup_controller.dart │ │ │ └── pages │ │ │ │ ├── setup.dart │ │ │ │ └── profile.dart │ │ ├── domain │ │ │ ├── use_cases │ │ │ │ ├── update.dart │ │ │ │ └── retrieve.dart │ │ │ ├── repositories │ │ │ │ └── profile_repository.dart │ │ │ └── entities │ │ │ │ ├── profile.dart │ │ │ │ └── profile.g.dart │ │ └── data │ │ │ ├── services │ │ │ └── profile_service.dart │ │ │ ├── local │ │ │ └── data_sources │ │ │ │ └── profile_local_database.dart │ │ │ ├── remote │ │ │ └── data_sources │ │ │ │ └── profile_remote_database.dart │ │ │ └── repositories │ │ │ └── profile_repository_impl.dart │ ├── home │ │ └── presentation │ │ │ ├── manager │ │ │ ├── home_binding.dart │ │ │ └── home_controller.dart │ │ │ └── pages │ │ │ └── home.dart │ ├── auth │ │ ├── presentation │ │ │ ├── manager │ │ │ │ ├── auth_binding.dart │ │ │ │ └── auth_controller.dart │ │ │ └── pages │ │ │ │ └── phone_auth.dart │ │ ├── domain │ │ │ └── repositories │ │ │ │ └── auth_repository.dart │ │ └── data │ │ │ ├── services │ │ │ └── auth_service.dart │ │ │ └── repositories │ │ │ └── auth_repository_impl.dart │ ├── onboarding │ │ └── presentation │ │ │ ├── manager │ │ │ ├── onboarding_binding.dart │ │ │ └── onboarding_controller.dart │ │ │ └── pages │ │ │ └── onboarding.dart │ └── settings │ │ └── presentation │ │ ├── manager │ │ ├── settings_binding.dart │ │ └── settings_controller.dart │ │ └── widgets │ │ ├── user_avatar.dart │ │ └── no_avatar.dart ├── shared │ ├── utils │ │ ├── copy_to_clipboard.dart │ │ ├── launch_url.dart │ │ ├── upload_image.dart │ │ ├── pick_file.dart │ │ ├── generate_file_icon.dart │ │ └── pick_image.dart │ ├── extensions │ │ ├── strings.dart │ │ └── buttons.dart │ ├── usecase │ │ └── usecase.dart │ ├── error │ │ ├── failure.dart │ │ └── exception.dart │ ├── network │ │ └── network.dart │ ├── ui │ │ ├── empty_state.dart │ │ ├── custom_image.dart │ │ ├── custom_avatar.dart │ │ ├── spinner.dart │ │ ├── custom_listtile.dart │ │ ├── snackbars.dart │ │ └── custom_bottomsheet.dart │ └── validation │ │ └── validator.dart ├── generated │ └── assets.dart ├── routes │ ├── app_routes.dart │ └── app_pages.dart ├── services │ └── init_services.dart ├── main.dart ├── firebase_options.dart └── translations │ └── translation.dart ├── storage.rules ├── firestore.rules ├── firebase.json ├── README.md ├── .gitignore ├── .metadata ├── analysis_options.yaml └── pubspec.yaml /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "studyhive-og" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /assets/members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/assets/members.png -------------------------------------------------------------------------------- /assets/no_hive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/assets/no_hive.png -------------------------------------------------------------------------------- /assets/hive_work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/assets/hive_work.png -------------------------------------------------------------------------------- /assets/discussion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/assets/discussion.png -------------------------------------------------------------------------------- /assets/onboarding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/assets/onboarding.gif -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/manager/join_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class JoinHiveController extends GetxController {} 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeronasiedu/studyhive/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read, write: if request.auth != null; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/shared/utils/copy_to_clipboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | void copyToClipboard(String text) async { 4 | ClipboardData data = ClipboardData(text: text); 5 | await Clipboard.setData(data); 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/jerondev/studyhive/studyhive/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jerondev.studyhive.studyhive 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | 2 | rules_version = '2'; 3 | service cloud.firestore { 4 | match /databases/{database}/documents { 5 | match /{document=**} { 6 | allow read, write: if request.auth != null; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/profile/presentation/manager/profile_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import '../../domain/entities/profile.dart'; 4 | 5 | class ProfileController extends GetxController { 6 | final Profile profile = Get.arguments; 7 | } 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /lib/src/home/presentation/manager/home_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'home_controller.dart'; 4 | 5 | class HomeBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => HomeController()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/auth/presentation/manager/auth_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'auth_controller.dart'; 4 | 5 | class AuthBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => AuthController(Get.find())); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/bindings/join_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import '../manager/join_controller.dart'; 4 | 5 | class JoinHiveBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => JoinHiveController()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:480976755846:ios:1c8abd08c81a5382c40507", 5 | "FIREBASE_PROJECT_ID": "studyhive-og", 6 | "GCM_SENDER_ID": "480976755846" 7 | } -------------------------------------------------------------------------------- /lib/src/onboarding/presentation/manager/onboarding_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'onboarding_controller.dart'; 4 | 5 | class OnboardingBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => OnboardingController(Get.find())); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/src/settings/presentation/manager/settings_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/settings/presentation/manager/settings_controller.dart'; 3 | 4 | class SettingsBinding extends Bindings { 5 | @override 6 | void dependencies() { 7 | Get.lazyPut(() => SettingsController()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/generated/assets.dart: -------------------------------------------------------------------------------- 1 | class Assets { 2 | Assets._(); 3 | 4 | static const String discussion = 'assets/discussion.png'; 5 | static const String hiveWork = 'assets/hive_work.png'; 6 | static const String onboarding = 'assets/onboarding.gif'; 7 | static const String members = 'assets/members.png'; 8 | static const String noHive = 'assets/no_hive.png'; 9 | } 10 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/shared/utils/launch_url.dart: -------------------------------------------------------------------------------- 1 | import 'package:url_launcher/url_launcher.dart'; 2 | 3 | import '../ui/snackbars.dart'; 4 | 5 | void openLink(String url) async { 6 | if (!url.startsWith("http")) { 7 | url = "https://$url"; 8 | } 9 | if (!await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication)) { 10 | showErrorSnackbar(message: "Couldn't open link \n try copying it instead"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/routes/app_routes.dart: -------------------------------------------------------------------------------- 1 | part of './app_pages.dart'; 2 | 3 | abstract class AppRoutes { 4 | static const onboarding = '/'; 5 | static const home = '/home'; 6 | static const phoneAuth = '/phoneAuth'; 7 | static const createHive = '/createHive'; 8 | static const settings = '/settings'; 9 | static const setupProfile = '/setupProfile'; 10 | static const profile = '/profile'; 11 | static const hive = '/hive/:id'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/shared/extensions/strings.dart: -------------------------------------------------------------------------------- 1 | extension StringInitialsExtension on String { 2 | String get initials { 3 | List names = split(" "); 4 | String initials = ""; 5 | 6 | int numWords = names.length > 2 ? 2 : names.length; // use at most two names 7 | 8 | for (int i = 0; i < numWords; i++) { 9 | String initial = names[i][0].toUpperCase(); 10 | initials += initial; 11 | } 12 | 13 | return initials; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/bindings/create_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/hive/domain/use_cases/create.dart'; 3 | import 'package:studyhive/src/hive/presentation/manager/create_controller.dart'; 4 | 5 | class CreateHiveBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => CreateHive(Get.find())); 9 | Get.lazyPut(() => CreateHiveController(Get.find())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "storage": { 3 | "rules": "storage.rules" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules" 7 | }, 8 | "emulators": { 9 | "auth": { 10 | "port": 9099 11 | }, 12 | "firestore": { 13 | "port": 8080 14 | }, 15 | "storage": { 16 | "port": 9199 17 | }, 18 | "ui": { 19 | "enabled": true, 20 | "port": 4000 21 | }, 22 | "singleProjectMode": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/profile/presentation/manager/profile_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/profile/presentation/manager/profile_controller.dart'; 3 | import 'package:studyhive/src/profile/presentation/manager/setup_controller.dart'; 4 | 5 | class ProfileBinding extends Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => ProfileController()); 9 | Get.lazyPut(() => SetupProfileController()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/media.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:studyhive/shared/utils/pick_file.dart'; 3 | 4 | part 'media.freezed.dart'; 5 | part 'media.g.dart'; 6 | 7 | @freezed 8 | class Media with _$Media { 9 | const factory Media({ 10 | required String url, 11 | required FileTypeOption type, 12 | }) = _Media; 13 | factory Media.fromJson(Map json) => _$MediaFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/members.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:studyhive/generated/assets.dart'; 3 | 4 | import '../../../../shared/ui/empty_state.dart'; 5 | 6 | class MembersPage extends StatelessWidget { 7 | const MembersPage({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return const EmptyState( 12 | text: 'Invite your friends to join your hive', 13 | asset: Assets.members, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/shared/usecase/usecase.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | import '../error/failure.dart'; 4 | 5 | /// Blue print for all useCases in the app 6 | /// Mainly for usecases uses [Either] 7 | abstract class UseCase { 8 | /// Contract call method 9 | Future> call(Params params); 10 | } 11 | 12 | /// create a generic params for usecases 13 | class Params { 14 | const Params(this.data); 15 | 16 | final T data; 17 | } 18 | 19 | class NoParams { 20 | const NoParams(); 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/shared/error/failure.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | 5 | /// Generic Failure handler 6 | class Failure extends Equatable { 7 | /// Failure constructor 8 | const Failure(this.message); 9 | 10 | /// Error message 11 | final String message; 12 | 13 | @override 14 | String toString() => _utf8convert(message); 15 | 16 | static String _utf8convert(String text) { 17 | final bytes = text.codeUnits; 18 | return utf8.decode(bytes); 19 | } 20 | 21 | @override 22 | List get props => [message]; 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/profile/domain/use_cases/update.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/shared/usecase/usecase.dart'; 4 | 5 | import '../entities/profile.dart'; 6 | import '../repositories/profile_repository.dart'; 7 | 8 | class UpdateProfile implements UseCase> { 9 | final ProfileRepository repository; 10 | 11 | UpdateProfile(this.repository); 12 | 13 | @override 14 | Future> call(Params params) { 15 | return repository.update(params.data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/auth/domain/repositories/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | 4 | import '../../../profile/domain/entities/profile.dart'; 5 | 6 | abstract class AuthRepository { 7 | /// Authenticates the user using google 8 | Future> continueWithGoogle(Profile profile); 9 | 10 | /// Authenticates the user using 11 | Future> continueWithApple(Profile profile); 12 | 13 | Future> continueWithPhone(Profile profile); 14 | 15 | Future> signOut(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/hive/domain/use_cases/details.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/src/hive/domain/entities/hive.dart'; 4 | 5 | import '../../../../shared/usecase/usecase.dart'; 6 | import '../repositories/hive_repository.dart'; 7 | 8 | class HiveDetails implements UseCase, Params> { 9 | final HiveRepository repository; 10 | 11 | HiveDetails(this.repository); 12 | 13 | @override 14 | Future>> call(Params params) { 15 | return repository.details(params.data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/hive/domain/use_cases/create.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/shared/usecase/usecase.dart'; 4 | import 'package:studyhive/src/hive/domain/entities/hive.dart'; 5 | import 'package:studyhive/src/hive/domain/repositories/hive_repository.dart'; 6 | 7 | class CreateHive implements UseCase> { 8 | final HiveRepository repository; 9 | 10 | CreateHive(this.repository); 11 | 12 | @override 13 | Future> call(Params params) { 14 | return repository.create(params.data); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/shared/network/network.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:internet_connection_checker/internet_connection_checker.dart'; 4 | 5 | /// Checks for internet internet 6 | abstract class NetworkInfo { 7 | /// Verifies if device has internet connection. 8 | Future hasInternet(); 9 | } 10 | 11 | /// Implements [NetworkInfo] 12 | class NetworkInfoImpl implements NetworkInfo { 13 | @override 14 | Future hasInternet() async { 15 | try { 16 | final results = await InternetConnectionChecker().hasConnection; 17 | return results; 18 | } on SocketException catch (_) { 19 | return false; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/hive/domain/use_cases/list.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/shared/usecase/usecase.dart'; 4 | import 'package:studyhive/src/hive/domain/entities/hive.dart'; 5 | import 'package:studyhive/src/hive/domain/repositories/hive_repository.dart'; 6 | 7 | class ListHives implements UseCase>, Params> { 8 | final HiveRepository repository; 9 | 10 | ListHives(this.repository); 11 | 12 | @override 13 | Future>>> call(Params params) { 14 | return repository.list(params.data); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/bindings/hive_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/hive/presentation/manager/discussions_controller.dart'; 3 | import 'package:studyhive/src/hive/presentation/manager/hive_controller.dart'; 4 | import 'package:studyhive/src/hive/presentation/manager/poll_controller.dart'; 5 | import 'package:studyhive/src/hive/presentation/manager/questions_controller.dart'; 6 | 7 | class HiveBinding extends Bindings { 8 | @override 9 | void dependencies() { 10 | Get.lazyPut(() => HiveController()); 11 | Get.put(DiscussionsController()); 12 | Get.put(PollController()); 13 | Get.put(QuestionsController()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/profile/domain/use_cases/retrieve.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/src/profile/domain/entities/profile.dart'; 4 | import 'package:studyhive/src/profile/domain/repositories/profile_repository.dart'; 5 | 6 | import '../../../../shared/usecase/usecase.dart'; 7 | 8 | class RetrieveProfile implements UseCase> { 9 | final ProfileRepository repository; 10 | 11 | RetrieveProfile(this.repository); 12 | 13 | @override 14 | Future> call(Params params) async { 15 | return await repository.retrieve(params.data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/src/auth/data/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/shared/network/network.dart'; 3 | 4 | import '../../../profile/data/local/data_sources/profile_local_database.dart'; 5 | import '../../../profile/data/remote/data_sources/profile_remote_database.dart'; 6 | import '../../domain/repositories/auth_repository.dart'; 7 | import '../repositories/auth_repository_impl.dart'; 8 | 9 | class AuthService extends GetxService { 10 | Future init() async { 11 | Get.put(AuthRepositoryImpl( 12 | remoteDatabase: Get.find(), 13 | localDatabase: Get.find(), 14 | networkInfo: Get.find(), 15 | )); 16 | return this; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # studyhive 2 | 3 | Welcome to Study Hive! This app is designed to help students form study groups, organize their work and assignments, schedule meetings, and more. With Study Hive, you can collaborate with your peers and improve your academic performance 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /lib/shared/error/exception.dart: -------------------------------------------------------------------------------- 1 | /// [Exception] thrown for server related error and device error 2 | class DeviceException implements Exception { 3 | /// Constructor for exceptions 4 | 5 | /// Error message 6 | final String message; 7 | 8 | /// Error code 9 | final int statusCode; 10 | 11 | DeviceException(this.message, {this.statusCode = 404}); 12 | 13 | /// Convert error messages from database 14 | factory DeviceException.fromJson(Map json, 15 | {int code = 404}) => 16 | DeviceException(json['detail'] as String, statusCode: code); 17 | 18 | @override 19 | String toString() => message; 20 | } 21 | 22 | class AlreadyAMemberException implements Exception { 23 | final String message; 24 | AlreadyAMemberException(this.message); 25 | @override 26 | String toString() => message; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/hive/domain/use_cases/post_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/src/hive/domain/entities/message.dart'; 4 | 5 | import '../../../../shared/usecase/usecase.dart'; 6 | import '../repositories/hive_repository.dart'; 7 | 8 | class PostMessage implements UseCase> { 9 | final HiveRepository repository; 10 | 11 | PostMessage(this.repository); 12 | 13 | @override 14 | Future> call(Params params) { 15 | return repository.postMessage(hiveId: params.data.hiveId, message: params.data.message); 16 | } 17 | } 18 | 19 | class PostMessageParams { 20 | final String hiveId; 21 | final Message message; 22 | 23 | PostMessageParams(this.hiveId, this.message); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/profile/domain/repositories/profile_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/src/profile/domain/entities/profile.dart'; 4 | 5 | abstract class ProfileRepository { 6 | /// Returns the [Profile] of the current user if connected to the internet else returns the [Profile] from the local database 7 | Future> retrieve(String userId); 8 | 9 | /// Saves the [Profile] to the local and remote database 10 | Future> save(Profile profile); 11 | 12 | /// Deletes the [Profile] from the local and remote database 13 | Future> delete(String userId); 14 | 15 | /// Updates the [Profile] to the local and remote database 16 | Future> update(Profile profile); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/media.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'media.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Media _$$_MediaFromJson(Map json) => _$_Media( 10 | url: json['url'] as String, 11 | type: $enumDecode(_$FileTypeOptionEnumMap, json['type']), 12 | ); 13 | 14 | Map _$$_MediaToJson(_$_Media instance) => { 15 | 'url': instance.url, 16 | 'type': _$FileTypeOptionEnumMap[instance.type]!, 17 | }; 18 | 19 | const _$FileTypeOptionEnumMap = { 20 | FileTypeOption.image: 'image', 21 | FileTypeOption.video: 'video', 22 | FileTypeOption.document: 'document', 23 | }; 24 | -------------------------------------------------------------------------------- /lib/shared/utils/upload_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:firebase_storage/firebase_storage.dart'; 4 | 5 | Future uploadImage({required String imagePath, String? folder}) async { 6 | try { 7 | File file = File(imagePath); 8 | final String fileName = file.path.split('/').last; 9 | FirebaseStorage storage = FirebaseStorage.instance; 10 | final path = folder != null ? '$folder/$fileName' : fileName; 11 | TaskSnapshot taskSnapshot = await storage.ref().child(path + DateTime.now().toString()).putFile(file); 12 | if (taskSnapshot.state == TaskState.success) { 13 | final String downloadUrl = await taskSnapshot.ref.getDownloadURL(); 14 | return downloadUrl; 15 | } 16 | return null; 17 | } catch (e) { 18 | print('Error while uploading image to firebase storage: $e'); 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/shared/ui/empty_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyState extends StatelessWidget { 4 | const EmptyState({super.key, required this.text, required this.asset, this.width}); 5 | 6 | final String text; 7 | final String asset; 8 | final double? width; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Center( 13 | child: Column( 14 | mainAxisSize: MainAxisSize.min, 15 | children: [ 16 | Padding( 17 | padding: const EdgeInsets.only(bottom: 8.0), 18 | child: Image.asset( 19 | asset, 20 | width: width ?? 250, 21 | ), 22 | ), 23 | Text( 24 | text, 25 | style: Theme.of(context).textTheme.titleSmall, 26 | ), 27 | ], 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/shared/ui/custom_image.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:studyhive/shared/ui/spinner.dart'; 5 | 6 | class CustomImage extends StatelessWidget { 7 | const CustomImage({ 8 | Key? key, 9 | required this.imageUrl, 10 | this.width, 11 | this.fit, 12 | }) : super(key: key); 13 | final String imageUrl; 14 | final double? width; 15 | final BoxFit? fit; 16 | @override 17 | Widget build(BuildContext context) { 18 | return CachedNetworkImage( 19 | imageUrl: imageUrl, 20 | placeholder: (context, url) => const Spinner(), 21 | errorWidget: (context, url, error) => const Icon(Icons.error), 22 | width: width, 23 | fit: fit ?? BoxFit.cover, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/manager/questions_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import '../../../../shared/utils/pick_file.dart'; 4 | import '../../domain/entities/message.dart'; 5 | 6 | class QuestionsController extends GetxController { 7 | Rx questionType = QuestionType.shortAnswer.obs; 8 | 9 | Future chooseImage() async { 10 | final results = await pickFile(option: FileTypeOption.image, dialogTitle: "Choose Image"); 11 | if (results != null) {} 12 | } 13 | 14 | Future chooseVideo() async { 15 | final results = await pickFile(option: FileTypeOption.video, dialogTitle: "Choose Video"); 16 | if (results != null) {} 17 | } 18 | 19 | Future chooseDocument() async { 20 | final results = await pickFile(option: FileTypeOption.document, dialogTitle: "Choose Document"); 21 | if (results != null) {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/topic.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'topic.freezed.dart'; 4 | part 'topic.g.dart'; 5 | 6 | @freezed 7 | class Topic with _$Topic { 8 | const factory Topic({ 9 | /// Unique ID of the topic 10 | required String id, 11 | 12 | /// Name of the topic 13 | required String name, 14 | 15 | /// Description of the topic 16 | String? description, 17 | 18 | /// The ID of the user who created the topic 19 | required String createdBy, 20 | 21 | /// The date the topic was created 22 | required DateTime createdAt, 23 | }) = _Topic; 24 | 25 | factory Topic.fromJson(Map json) => _$TopicFromJson(json); 26 | 27 | factory Topic.empty() => Topic( 28 | id: '', 29 | name: '', 30 | createdBy: '', 31 | createdAt: DateTime.now(), 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/topic.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'topic.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Topic _$$_TopicFromJson(Map json) => _$_Topic( 10 | id: json['id'] as String, 11 | name: json['name'] as String, 12 | description: json['description'] as String?, 13 | createdBy: json['createdBy'] as String, 14 | createdAt: DateTime.parse(json['createdAt'] as String), 15 | ); 16 | 17 | Map _$$_TopicToJson(_$_Topic instance) => { 18 | 'id': instance.id, 19 | 'name': instance.name, 20 | 'description': instance.description, 21 | 'createdBy': instance.createdBy, 22 | 'createdAt': instance.createdAt.toIso8601String(), 23 | }; 24 | -------------------------------------------------------------------------------- /lib/src/profile/domain/entities/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'profile.freezed.dart'; 4 | part 'profile.g.dart'; 5 | 6 | @Freezed(makeCollectionsUnmodifiable: false) 7 | class Profile with _$Profile { 8 | const factory Profile({ 9 | /// Unique ID of the user 10 | required String id, 11 | 12 | /// Name of the user 13 | required String name, 14 | 15 | /// Email of the user 16 | String? email, 17 | 18 | /// Photo URL of the user 19 | String? photoUrl, 20 | 21 | /// Bio of the user 22 | String? bio, 23 | 24 | /// School of the user 25 | String? school, 26 | 27 | /// Phone number of the user 28 | String? phoneNumber, 29 | }) = _Profile; 30 | 31 | factory Profile.fromJson(Map json) => _$ProfileFromJson(json); 32 | 33 | factory Profile.empty() => const Profile( 34 | id: '', 35 | name: '', 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /lib/shared/utils/pick_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | 5 | enum FileTypeOption { 6 | image, 7 | video, 8 | document, 9 | } 10 | 11 | List getAllowedExtensions(FileTypeOption option) { 12 | switch (option) { 13 | case FileTypeOption.image: 14 | return ['jpg', 'png', 'jpeg']; 15 | case FileTypeOption.video: 16 | return ['mp4', 'mov', 'avi']; 17 | case FileTypeOption.document: 18 | return ['pdf', 'doc', 'docx']; 19 | } 20 | } 21 | 22 | Future pickFile({required FileTypeOption option, String dialogTitle = "Choose file"}) async { 23 | FilePickerResult? result = await FilePicker.platform.pickFiles( 24 | type: FileType.custom, 25 | allowedExtensions: getAllowedExtensions(option), 26 | dialogTitle: dialogTitle, 27 | ); 28 | 29 | if (result != null) { 30 | return File(result.files.single.path!); 31 | } else { 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/shared/utils/generate_file_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:iconly/iconly.dart'; 3 | import 'package:studyhive/shared/utils/pick_file.dart'; 4 | 5 | Icon generateFileIcon(FileTypeOption option) { 6 | switch (option) { 7 | case FileTypeOption.image: 8 | return const Icon(IconlyLight.image); 9 | case FileTypeOption.video: 10 | return const Icon(IconlyLight.play); 11 | case FileTypeOption.document: 12 | return const Icon(IconlyLight.document); 13 | } 14 | } 15 | 16 | Icon generateIconFromExtension(String extension) { 17 | switch (extension) { 18 | case 'jpg': 19 | case 'jpeg': 20 | case 'png': 21 | return const Icon(IconlyLight.image); 22 | case 'mp4': 23 | case 'mov': 24 | case 'avi': 25 | return const Icon(IconlyLight.play); 26 | case 'pdf': 27 | case 'doc': 28 | case 'docx': 29 | return const Icon(IconlyLight.document); 30 | default: 31 | return const Icon(IconlyLight.paper); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.0' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | // END: FlutterFire Configuration 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | // ... other dependencies such as 'com.google.gms:google-services' 15 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | mavenCentral() 23 | } 24 | } 25 | 26 | rootProject.buildDir = '../build' 27 | subprojects { 28 | project.buildDir = "${rootProject.buildDir}/${project.name}" 29 | } 30 | subprojects { 31 | project.evaluationDependsOn(':app') 32 | } 33 | 34 | tasks.register("clean", Delete) { 35 | delete rootProject.buildDir 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/onboarding/presentation/manager/onboarding_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/routes/app_pages.dart'; 3 | import 'package:studyhive/shared/ui/snackbars.dart'; 4 | import 'package:studyhive/src/auth/domain/repositories/auth_repository.dart'; 5 | 6 | import '../../../profile/domain/entities/profile.dart'; 7 | 8 | class OnboardingController extends GetxController { 9 | final AuthRepository _authRepository; 10 | 11 | OnboardingController(this._authRepository); 12 | 13 | final RxBool loading = false.obs; 14 | 15 | Future signInWithGoogle() async { 16 | loading.value = true; 17 | final results = await _authRepository.continueWithGoogle(Profile.empty()); 18 | results.fold((failure) { 19 | loading.value = false; 20 | showErrorSnackbar(message: failure.message); 21 | }, (exists) { 22 | loading.value = false; 23 | if (exists) { 24 | Get.offAllNamed(AppRoutes.home); 25 | } else { 26 | Get.offAllNamed(AppRoutes.setupProfile); 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/profile/domain/entities/profile.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'profile.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Profile _$$_ProfileFromJson(Map json) => _$_Profile( 10 | id: json['id'] as String, 11 | name: json['name'] as String, 12 | email: json['email'] as String?, 13 | photoUrl: json['photoUrl'] as String?, 14 | bio: json['bio'] as String?, 15 | school: json['school'] as String?, 16 | phoneNumber: json['phoneNumber'] as String?, 17 | ); 18 | 19 | Map _$$_ProfileToJson(_$_Profile instance) => 20 | { 21 | 'id': instance.id, 22 | 'name': instance.name, 23 | 'email': instance.email, 24 | 'photoUrl': instance.photoUrl, 25 | 'bio': instance.bio, 26 | 'school': instance.school, 27 | 'phoneNumber': instance.phoneNumber, 28 | }; 29 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/shared/ui/custom_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:studyhive/shared/ui/spinner.dart'; 4 | 5 | class CustomAvatar extends StatelessWidget { 6 | const CustomAvatar({ 7 | Key? key, 8 | required this.imageUrl, 9 | this.width, 10 | this.radius, 11 | this.fit, 12 | this.spinnerSize, 13 | }) : super(key: key); 14 | final String imageUrl; 15 | final double? width; 16 | final double? radius; 17 | final BoxFit? fit; 18 | final SpinnerSize? spinnerSize; 19 | @override 20 | Widget build(BuildContext context) { 21 | return CachedNetworkImage( 22 | imageUrl: imageUrl, 23 | imageBuilder: (context, imageProvider) => CircleAvatar( 24 | radius: radius, 25 | backgroundImage: imageProvider, 26 | ), 27 | placeholder: (context, url) => Spinner( 28 | size: spinnerSize, 29 | ), 30 | errorWidget: (context, url, error) => const Icon(Icons.error), 31 | width: width, 32 | fit: fit ?? BoxFit.cover, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/services/init_services.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:get_storage/get_storage.dart'; 3 | import 'package:studyhive/src/auth/data/services/auth_service.dart'; 4 | import 'package:studyhive/src/hive/data/hive_service.dart'; 5 | import 'package:studyhive/src/profile/data/services/profile_service.dart'; 6 | 7 | import '../shared/network/network.dart'; 8 | 9 | Future initServices() async { 10 | await Get.putAsync(() => NetworkServices().init()); 11 | await Get.putAsync(() => ProfileService().init()); 12 | await Get.putAsync(() => AuthService().init()); 13 | await Get.putAsync(() => HiveService().init()); 14 | await Get.putAsync(() => LocalDatabaseServices().init()); 15 | } 16 | 17 | class LocalDatabaseServices extends GetxService { 18 | Future init() async { 19 | await GetStorage.init('profileBox'); 20 | await GetStorage.init('hiveBox'); 21 | return this; 22 | } 23 | } 24 | 25 | class NetworkServices extends GetxService { 26 | Future init() async { 27 | Get.put(NetworkInfoImpl()); 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/profile/data/services/profile_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/profile/domain/use_cases/update.dart'; 3 | 4 | import '../../../../shared/network/network.dart'; 5 | import '../../domain/repositories/profile_repository.dart'; 6 | import '../../domain/use_cases/retrieve.dart'; 7 | import '../local/data_sources/profile_local_database.dart'; 8 | import '../remote/data_sources/profile_remote_database.dart'; 9 | import '../repositories/profile_repository_impl.dart'; 10 | 11 | class ProfileService extends GetxService { 12 | Future init() async { 13 | Get.put(ProfileLocalDatabaseImpl()); 14 | Get.put(ProfileRemoteDatabaseImpl()); 15 | Get.put(ProfileRepositoryImpl( 16 | remoteDatabase: Get.find(), 17 | localDatabase: Get.find(), 18 | networkInfo: Get.find(), 19 | )); 20 | Get.put(RetrieveProfile(Get.find())); 21 | Get.put(UpdateProfile(Get.find())); 22 | return this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "480976755846", 4 | "project_id": "studyhive-og", 5 | "storage_bucket": "studyhive-og.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:480976755846:android:ef9886b15773dc76c40507", 11 | "android_client_info": { 12 | "package_name": "com.jerondev.studyhive.studyhive" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "480976755846-19gubt7v7ougktudgtcbd7dgsao1283t.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyA80i33kNWH1kJ59S6orqaH4Jhps9hTXhc" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "480976755846-19gubt7v7ougktudgtcbd7dgsao1283t.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /lib/shared/extensions/buttons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get_rx/src/rx_types/rx_types.dart'; 3 | import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; 4 | import 'package:studyhive/shared/ui/spinner.dart'; 5 | 6 | extension LoadingElevatedButtonExtension on ElevatedButton { 7 | Widget withLoading({RxBool? loading, Icon? icon, String? text}) { 8 | var loadState = loading ?? RxBool(false); 9 | 10 | assert(icon == null && text == null || icon != null && text != null, 11 | 'ElevatedButton must have both icon and label, or neither.'); 12 | 13 | if (icon != null) { 14 | return Obx(() => loadState.value 15 | ? ElevatedButton.icon( 16 | onPressed: null, 17 | label: Text(text ?? ''), 18 | icon: const Spinner( 19 | size: SpinnerSize.sm, 20 | ), 21 | ) 22 | : this); 23 | } 24 | return Obx(() => loadState.value 25 | ? const ElevatedButton( 26 | onPressed: null, 27 | child: Spinner( 28 | size: SpinnerSize.sm, 29 | ), 30 | ) 31 | : this); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/shared/ui/spinner.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:get/get.dart'; 6 | 7 | class Spinner extends StatelessWidget { 8 | const Spinner({ 9 | Key? key, 10 | this.size, 11 | this.color, 12 | }) : super(key: key); 13 | final SpinnerSize? size; 14 | final Color? color; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | double spinSize = 45; 19 | if (size == SpinnerSize.sm) { 20 | spinSize = 20; 21 | } else if (size == SpinnerSize.md) { 22 | spinSize = 30; 23 | } else if (size == SpinnerSize.lg) { 24 | spinSize = 60; 25 | } 26 | 27 | return SizedBox.square( 28 | dimension: spinSize, 29 | child: Center( 30 | child: CircularProgressIndicator.adaptive( 31 | valueColor: AlwaysStoppedAnimation(color ?? Get.theme.colorScheme.primary), 32 | strokeWidth: 2.5, 33 | backgroundColor: Platform.isIOS ? Get.theme.colorScheme.primary : null, 34 | value: null, 35 | semanticsLabel: 'Loading', 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | 42 | enum SpinnerSize { sm, md, lg } 43 | -------------------------------------------------------------------------------- /lib/src/hive/domain/repositories/hive_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:studyhive/shared/error/failure.dart'; 3 | import 'package:studyhive/src/hive/domain/entities/hive.dart'; 4 | import 'package:studyhive/src/hive/domain/entities/message.dart'; 5 | 6 | abstract class HiveRepository { 7 | /// Returns a list of all the [Hive]s 8 | Future>>> list(String userId); 9 | 10 | /// Creates a new [Hive] 11 | Future> create(Hive hive); 12 | 13 | /// Updates an existing [Hive] 14 | Future> update(Hive hive); 15 | 16 | /// Deletes an existing [Hive] 17 | Future> delete(String hiveId); 18 | 19 | /// Joins an existing [Hive] 20 | Future> join({required String hiveId, required String userId}); 21 | 22 | /// Leaves an existing [Hive] 23 | Future> leave({required String hiveId, required String userId}); 24 | 25 | /// Returns details of a [Hive] 26 | Future>> details(String hiveId); 27 | 28 | /// Post a message to a [Hive] 29 | Future> postMessage({required String hiveId, required Message message}); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/hive/data/hive_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/hive/data/remote/data_sources/hive_remote_database.dart'; 3 | import 'package:studyhive/src/hive/data/repositories/hive_repository_impl.dart'; 4 | import 'package:studyhive/src/hive/domain/use_cases/details.dart'; 5 | import 'package:studyhive/src/hive/domain/use_cases/list.dart'; 6 | import 'package:studyhive/src/hive/domain/use_cases/post_message.dart'; 7 | 8 | import '../../../shared/network/network.dart'; 9 | import '../domain/repositories/hive_repository.dart'; 10 | import 'local/data_sources/hive_local_database.dart'; 11 | 12 | class HiveService extends GetxService { 13 | Future init() async { 14 | Get.put(HiveRemoteDatabaseImpl(Get.find())); 15 | Get.put(HiveLocalDatabaseImpl()); 16 | Get.put(HiveRepositoryImpl( 17 | remoteDatabase: Get.find(), 18 | localDatabase: Get.find(), 19 | networkInfo: Get.find(), 20 | )); 21 | Get.put(ListHives(Get.find())); 22 | Get.put(HiveDetails(Get.find())); 23 | Get.put(PostMessage(Get.find())); 24 | return this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 480976755846-vbej0t2b5kvbbh3vf047sfk4028hausd.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.480976755846-vbej0t2b5kvbbh3vf047sfk4028hausd 9 | API_KEY 10 | AIzaSyCGZMjju3aukPvqzqPaBd6dEftlv0opWmU 11 | GCM_SENDER_ID 12 | 480976755846 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.jerondev.studyhive.studyhive 17 | PROJECT_ID 18 | studyhive-og 19 | STORAGE_BUCKET 20 | studyhive-og.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:480976755846:ios:1c8abd08c81a5382c40507 33 | 34 | -------------------------------------------------------------------------------- /lib/shared/ui/custom_listtile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:studyhive/shared/ui/custom_avatar.dart'; 3 | 4 | class CustomListTile extends StatelessWidget { 5 | const CustomListTile({ 6 | Key? key, 7 | required this.onTap, 8 | required this.title, 9 | this.subtitle, 10 | this.trailing, 11 | this.leading, 12 | this.url, 13 | }) : super(key: key); 14 | final VoidCallback onTap; 15 | final String title; 16 | final String? subtitle; 17 | final Widget? trailing; 18 | final Widget? leading; 19 | final String? url; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | // assert if url is not null then 24 | return ListTile( 25 | onTap: onTap, 26 | leading: url == null 27 | ? CircleAvatar( 28 | backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), 29 | child: leading, 30 | ) 31 | : CustomAvatar( 32 | imageUrl: url!, 33 | ), 34 | title: Text( 35 | title, 36 | style: Theme.of(context).textTheme.titleSmall!.copyWith( 37 | fontWeight: FontWeight.bold, 38 | ), 39 | ), 40 | subtitle: subtitle != null ? Text(subtitle!) : null, 41 | trailing: trailing, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 17 | base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 18 | - platform: android 19 | create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 20 | base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 21 | - platform: ios 22 | create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 23 | base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 24 | - platform: linux 25 | create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 26 | base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/manager/discussions_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:studyhive/shared/utils/pick_file.dart'; 6 | 7 | class DiscussionsController extends GetxController { 8 | final textEditingController = TextEditingController(); 9 | RxBool canPost = false.obs; 10 | RxList attachments = [].obs; 11 | 12 | @override 13 | void onInit() { 14 | super.onInit(); 15 | textEditingController.addListener(() { 16 | canPost.value = textEditingController.text.isNotEmpty; 17 | }); 18 | } 19 | 20 | Future chooseImage() async { 21 | final results = await pickFile(option: FileTypeOption.image, dialogTitle: "Choose Image"); 22 | if (results != null) { 23 | attachments.add(results); 24 | } 25 | } 26 | 27 | Future chooseVideo() async { 28 | final results = await pickFile(option: FileTypeOption.video, dialogTitle: "Choose Video"); 29 | if (results != null) { 30 | attachments.add(results); 31 | } 32 | } 33 | 34 | Future chooseDocument() async { 35 | final results = await pickFile(option: FileTypeOption.document, dialogTitle: "Choose Document"); 36 | if (results != null) { 37 | attachments.add(results); 38 | } 39 | } 40 | 41 | Future post() async {} 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/home/presentation/manager/home_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:internet_connection_checker/internet_connection_checker.dart'; 6 | import 'package:studyhive/shared/ui/snackbars.dart'; 7 | import 'package:studyhive/src/hive/domain/use_cases/list.dart'; 8 | 9 | import '../../../../shared/usecase/usecase.dart'; 10 | import '../../../hive/domain/entities/hive.dart'; 11 | 12 | class HomeController extends GetxController { 13 | final listHives = Get.find(); 14 | final user = FirebaseAuth.instance.currentUser!; 15 | 16 | @override 17 | void onInit() async { 18 | InternetConnectionChecker().onStatusChange.listen((status) { 19 | switch (status) { 20 | case InternetConnectionStatus.connected: 21 | Get.closeAllSnackbars(); 22 | break; 23 | case InternetConnectionStatus.disconnected: 24 | showLoadingSnackbar(message: "Trying to reconnect..."); 25 | } 26 | }); 27 | super.onInit(); 28 | } 29 | 30 | Stream> list() async* { 31 | final result = await listHives(Params(user.uid)); 32 | yield* result.fold((l) async* { 33 | showErrorSnackbar(message: l.message); 34 | yield []; 35 | }, (r) async* { 36 | yield* r; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/hive/data/local/data_sources/hive_local_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_storage/get_storage.dart'; 2 | 3 | import '../../../domain/entities/hive.dart'; 4 | 5 | abstract class HiveLocalDatabase { 6 | /// Returns a list of all the Hives 7 | Stream> list(); 8 | 9 | /// Saves a list of Hives 10 | Future save(List hives); 11 | 12 | /// Retrieves a Hive 13 | Future retrieve(String hiveId); 14 | } 15 | 16 | class HiveLocalDatabaseImpl implements HiveLocalDatabase { 17 | final boxName = 'hiveBox'; 18 | final hivesContainer = 'hives'; 19 | 20 | @override 21 | Stream> list() async* { 22 | final box = GetStorage(boxName); 23 | final hives = box.read(hivesContainer); 24 | final List data = hives != null ? hives.map((hive) => Hive.fromJson(hive)).toList() : []; 25 | yield data; 26 | } 27 | 28 | @override 29 | Future retrieve(String hiveId) async { 30 | final box = GetStorage(boxName); 31 | final hives = box.read(hivesContainer); 32 | final hive = hives.firstWhere((hive) => hive.id == hiveId); 33 | return hive; 34 | } 35 | 36 | @override 37 | Future save(List hives) async { 38 | final box = GetStorage(boxName); 39 | final hivesMap = hives.map((hive) => hive.toJson()).toList(); 40 | await box.write(hivesContainer, hivesMap); 41 | return 'Success'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/hive.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:studyhive/src/hive/domain/entities/message.dart'; 3 | 4 | part 'hive.freezed.dart'; 5 | part 'hive.g.dart'; 6 | 7 | @Freezed(makeCollectionsUnmodifiable: false) 8 | class Hive with _$Hive { 9 | const factory Hive({ 10 | /// Unique ID of the Hive 11 | required String id, 12 | 13 | /// Name of the Hive 14 | required String name, 15 | 16 | /// Description of the Hive 17 | String? description, 18 | 19 | /// Photo URL of the Hive 20 | String? photoUrl, 21 | 22 | /// Members of the Hive 23 | @Default([]) List members, 24 | 25 | /// The ID of the user who created the Hive 26 | required String createdBy, 27 | // The date the Hive was created 28 | required DateTime createdAt, 29 | // The date the Hive was last updated 30 | required DateTime updatedAt, 31 | 32 | /// conversations of the Hive 33 | @Default([]) List conversations, 34 | 35 | /// Admins of the Hive 36 | @Default([]) List admins, 37 | }) = _Hive; 38 | 39 | factory Hive.fromJson(Map json) => _$HiveFromJson(json); 40 | 41 | factory Hive.empty() => Hive( 42 | id: '', 43 | name: '', 44 | createdBy: '', 45 | createdAt: DateTime.now(), 46 | updatedAt: DateTime.now(), 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/manager/poll_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | import '../../../../shared/utils/pick_file.dart'; 5 | 6 | class PollController extends GetxController { 7 | final optionControllers = [].obs; 8 | final pollTextController = TextEditingController(); 9 | RxBool canPost = false.obs; 10 | 11 | @override 12 | void onInit() { 13 | super.onInit(); 14 | optionControllers.addAll([ 15 | TextEditingController(), 16 | TextEditingController(), 17 | ]); 18 | } 19 | 20 | void addOption() { 21 | optionControllers.add(TextEditingController()); 22 | } 23 | 24 | void removeOption(int index) { 25 | optionControllers.removeAt(index); 26 | } 27 | 28 | Future chooseImage() async { 29 | final results = await pickFile(option: FileTypeOption.image, dialogTitle: "Choose Image"); 30 | if (results != null) {} 31 | } 32 | 33 | Future chooseVideo() async { 34 | final results = await pickFile(option: FileTypeOption.video, dialogTitle: "Choose Video"); 35 | if (results != null) {} 36 | } 37 | 38 | Future chooseDocument() async { 39 | final results = await pickFile(option: FileTypeOption.document, dialogTitle: "Choose Document"); 40 | if (results != null) {} 41 | } 42 | 43 | @override 44 | void onClose() { 45 | pollTextController.dispose(); 46 | for (var option in optionControllers) { 47 | option.dispose(); 48 | } 49 | super.onClose(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: studyhive 2 | description: Welcome to Study Hive! This app is designed to help students form study groups, organize their work and assignments, schedule meetings, and more. With Study Hive, you can collaborate with your peers and improve your academic performance 3 | 4 | publish_to: 'none' 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=2.19.5 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | cupertino_icons: ^1.0.2 14 | google_fonts: 15 | flex_color_scheme: ^7.0.4 16 | equatable: 17 | get: 18 | ionicons: 19 | dartz: ^0.10.1 20 | url_launcher: 21 | cached_network_image: 22 | iconly: ^1.0.1 23 | google_sign_in: ^6.1.0 24 | firebase_core: ^2.10.0 25 | cloud_firestore: ^4.5.3 26 | firebase_auth: ^4.4.2 27 | freezed: ^2.3.2 28 | freezed_annotation: ^2.2.0 29 | json_annotation: ^4.8.0 30 | intl_phone_number_input: ^0.7.3+1 31 | get_storage: ^2.1.1 32 | pinput: ^2.2.31 33 | image_picker: ^0.8.7+4 34 | image_cropper: ^3.0.3 35 | internet_connection_checker: ^1.0.0+1 36 | nanoid: ^1.0.0 37 | firebase_storage: ^11.1.2 38 | flutter_animate: ^4.1.1+1 39 | skeletons: ^0.0.3 40 | rxdart: ^0.27.7 41 | # device_preview: ^1.1.0 42 | firebase_crashlytics: ^3.2.0 43 | file_picker: ^5.3.0 44 | open_filex: ^4.3.2 45 | animations: ^2.0.7 46 | 47 | dev_dependencies: 48 | flutter_test: 49 | sdk: flutter 50 | 51 | flutter_lints: ^2.0.1 52 | build_runner: ^2.3.3 53 | json_serializable: ^6.6.1 54 | 55 | flutter: 56 | uses-material-design: true 57 | 58 | 59 | assets: 60 | - assets/ 61 | 62 | -------------------------------------------------------------------------------- /lib/shared/ui/snackbars.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:ionicons/ionicons.dart'; 5 | 6 | showErrorSnackbar({required String message}) { 7 | return Get 8 | ..closeAllSnackbars() 9 | ..snackbar( 10 | "Error", 11 | message, 12 | backgroundColor: Get.theme.colorScheme.errorContainer, 13 | colorText: Get.theme.colorScheme.onErrorContainer, 14 | shouldIconPulse: true, 15 | barBlur: 10, 16 | icon: const Icon(Ionicons.warning_outline), 17 | ); 18 | } 19 | 20 | showSuccessSnackbar({required String message}) { 21 | return Get 22 | ..closeAllSnackbars() 23 | ..snackbar( 24 | "Success", 25 | message, 26 | backgroundColor: Get.theme.colorScheme.primaryContainer, 27 | colorText: Get.theme.colorScheme.onPrimaryContainer, 28 | icon: const Icon(Ionicons.checkmark_circle_outline), 29 | shouldIconPulse: true, 30 | ); 31 | } 32 | 33 | showLoadingSnackbar({required String message, bool isPersistent = false, title = "Loading"}) { 34 | return Get 35 | ..closeAllSnackbars() 36 | ..snackbar( 37 | title, 38 | message, 39 | backgroundColor: Get.theme.colorScheme.primaryContainer, 40 | colorText: Get.theme.colorScheme.onPrimaryContainer, 41 | showProgressIndicator: true, 42 | icon: const Icon(Ionicons.cloud_upload_outline), 43 | duration: isPersistent ? const Duration(days: 365) : const Duration(seconds: 5), 44 | isDismissible: !isPersistent, 45 | shouldIconPulse: true, 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/settings/presentation/widgets/user_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | 5 | import '../../../../shared/ui/custom_avatar.dart'; 6 | 7 | class UserAvatar extends StatelessWidget { 8 | const UserAvatar({Key? key, required this.url, this.size = UserAvatarSize.sm}) : super(key: key); 9 | final String url; 10 | final UserAvatarSize size; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final theme = Theme.of(context); 15 | return Stack( 16 | clipBehavior: Clip.none, 17 | children: [ 18 | CustomAvatar( 19 | radius: size == UserAvatarSize.sm ? 23 : 60, 20 | imageUrl: url, 21 | ), 22 | Positioned( 23 | bottom: size == UserAvatarSize.sm ? -5 : 1, 24 | right: size == UserAvatarSize.sm ? -2 : 1, 25 | child: CircleAvatar( 26 | radius: size == UserAvatarSize.sm ? 11 : 15, 27 | backgroundColor: Theme.of(context).colorScheme.background, 28 | child: CircleAvatar( 29 | radius: size == UserAvatarSize.sm ? 9 : 13, 30 | backgroundColor: Get.isDarkMode ? theme.colorScheme.secondary : theme.colorScheme.tertiary, 31 | child: Icon( 32 | IconlyLight.camera, 33 | color: theme.colorScheme.background, 34 | size: size == UserAvatarSize.sm ? 13 : 16, 35 | ), 36 | ), 37 | ), 38 | ) 39 | ], 40 | ); 41 | } 42 | } 43 | 44 | enum UserAvatarSize { 45 | sm, 46 | lg, 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/profile/presentation/manager/setup_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import '../../../../routes/app_pages.dart'; 6 | import '../../../../shared/usecase/usecase.dart'; 7 | import '../../domain/entities/profile.dart'; 8 | import '../../domain/use_cases/retrieve.dart'; 9 | import '../../domain/use_cases/update.dart'; 10 | 11 | class SetupProfileController extends GetxController { 12 | RxBool loading = false.obs; 13 | RxBool enableFinishButton = false.obs; 14 | final nameController = TextEditingController(); 15 | final updateProfileUseCase = Get.find(); 16 | final retrieveProfileUseCase = Get.find(); 17 | 18 | @override 19 | void onInit() { 20 | nameController.addListener(() { 21 | enableFinishButton.value = nameController.text.isNotEmpty; 22 | }); 23 | super.onInit(); 24 | } 25 | 26 | Future _retrieveProfile() async { 27 | final result = await retrieveProfileUseCase(Params(FirebaseAuth.instance.currentUser!.uid)); 28 | return result.fold((failure) => Profile.empty(), (profile) => profile); 29 | } 30 | 31 | Future updateProfile() async { 32 | loading.value = true; 33 | final profile = await _retrieveProfile(); 34 | final updatedProfile = profile.copyWith(name: nameController.text.trim()); 35 | final result = await updateProfileUseCase(Params(updatedProfile)); 36 | result.fold((failure) => loading.value = false, (profile) { 37 | loading.value = false; 38 | Get.offAllNamed(AppRoutes.home); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/profile/data/local/data_sources/profile_local_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_storage/get_storage.dart'; 2 | 3 | import '../../../domain/entities/profile.dart'; 4 | 5 | abstract class ProfileLocalDatabase { 6 | /// Saves the [Profile] to the local database 7 | Future save(Profile profile); 8 | 9 | /// Returns the [Profile] from the local database 10 | Future retrieve(); 11 | 12 | /// Deletes the [Profile] from the local database 13 | Future delete(); 14 | 15 | /// Returns true if the user is authenticated 16 | Future authStatus(); 17 | 18 | /// Checks if the user finished the setup process 19 | Future finishedSetup(); 20 | } 21 | 22 | class ProfileLocalDatabaseImpl implements ProfileLocalDatabase { 23 | final boxName = 'profileBox'; 24 | final userKey = 'profileKey'; 25 | 26 | @override 27 | Future delete() async { 28 | final box = GetStorage(boxName); 29 | await box.erase(); 30 | } 31 | 32 | @override 33 | Future retrieve() async { 34 | final box = GetStorage(boxName); 35 | final userDetails = box.read(userKey); 36 | final user = userDetails != null ? Profile.fromJson(userDetails) : Profile.empty(); 37 | return Future.value(user); 38 | } 39 | 40 | @override 41 | Future save(Profile profile) async { 42 | final box = GetStorage(boxName); 43 | await box.write(userKey, profile.toJson()); 44 | } 45 | 46 | @override 47 | Future authStatus() { 48 | final box = GetStorage(boxName); 49 | final user = box.read(userKey); 50 | return Future.value(user != null); 51 | } 52 | 53 | @override 54 | Future finishedSetup() async { 55 | final userDetails = await retrieve(); 56 | return userDetails.name.isNotEmpty; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/hive.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'hive.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Hive _$$_HiveFromJson(Map json) => _$_Hive( 10 | id: json['id'] as String, 11 | name: json['name'] as String, 12 | description: json['description'] as String?, 13 | photoUrl: json['photoUrl'] as String?, 14 | members: (json['members'] as List?) 15 | ?.map((e) => e as String) 16 | .toList() ?? 17 | const [], 18 | createdBy: json['createdBy'] as String, 19 | createdAt: DateTime.parse(json['createdAt'] as String), 20 | updatedAt: DateTime.parse(json['updatedAt'] as String), 21 | conversations: (json['conversations'] as List?) 22 | ?.map((e) => Message.fromJson(e as Map)) 23 | .toList() ?? 24 | const [], 25 | admins: (json['admins'] as List?) 26 | ?.map((e) => e as String) 27 | .toList() ?? 28 | const [], 29 | ); 30 | 31 | Map _$$_HiveToJson(_$_Hive instance) => { 32 | 'id': instance.id, 33 | 'name': instance.name, 34 | 'description': instance.description, 35 | 'photoUrl': instance.photoUrl, 36 | 'members': instance.members, 37 | 'createdBy': instance.createdBy, 38 | 'createdAt': instance.createdAt.toIso8601String(), 39 | 'updatedAt': instance.updatedAt.toIso8601String(), 40 | 'conversations': instance.conversations, 41 | 'admins': instance.admins, 42 | }; 43 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.generated_projects.each do |project| 39 | project.targets.each do |target| 40 | target.build_configurations.each do |config| 41 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 42 | end 43 | end 44 | end 45 | installer.pods_project.targets.each do |target| 46 | flutter_additional_ios_build_settings(target) 47 | end 48 | end -------------------------------------------------------------------------------- /lib/src/settings/presentation/widgets/no_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | 5 | class NoAvatar extends StatelessWidget { 6 | const NoAvatar({Key? key, required this.initials, this.size = NoAvatarSize.sm}) : super(key: key); 7 | final String initials; 8 | final NoAvatarSize size; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final theme = Theme.of(context); 13 | return Stack( 14 | clipBehavior: Clip.none, 15 | children: [ 16 | CircleAvatar( 17 | radius: size == NoAvatarSize.sm ? 23 : 60, 18 | child: Text( 19 | initials, 20 | style: size == NoAvatarSize.sm 21 | ? theme.textTheme.bodyMedium 22 | : theme.textTheme.titleLarge!.copyWith( 23 | fontSize: 30, 24 | fontWeight: FontWeight.bold, 25 | ), 26 | ), 27 | ), 28 | Positioned( 29 | bottom: size == NoAvatarSize.sm ? -5 : 1, 30 | right: size == NoAvatarSize.sm ? -2 : 1, 31 | child: CircleAvatar( 32 | radius: size == NoAvatarSize.sm ? 9 : 13, 33 | backgroundColor: Theme.of(context).colorScheme.background, 34 | child: CircleAvatar( 35 | radius: 9, 36 | backgroundColor: Get.isDarkMode ? theme.colorScheme.secondary : theme.colorScheme.tertiary, 37 | child: Icon( 38 | IconlyLight.camera, 39 | color: theme.colorScheme.background, 40 | size: size == NoAvatarSize.sm ? 13 : 16, 41 | ), 42 | ), 43 | ), 44 | ) 45 | ], 46 | ); 47 | } 48 | } 49 | 50 | enum NoAvatarSize { 51 | sm, 52 | lg, 53 | } 54 | -------------------------------------------------------------------------------- /lib/shared/ui/custom_bottomsheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | showCustomBottomSheet({ 5 | double? height, 6 | String? title, 7 | required Widget child, 8 | bool isScrollControlled = false, 9 | double horizontalPadding = 10, 10 | }) { 11 | return Get.bottomSheet( 12 | SizedBox( 13 | height: height ?? Get.height * 0.25, 14 | child: Padding( 15 | padding: EdgeInsets.only( 16 | top: 6, 17 | left: horizontalPadding, 18 | right: horizontalPadding, 19 | bottom: 10, 20 | ), 21 | child: Column( 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | children: [ 24 | Center( 25 | child: Container( 26 | width: 55, 27 | height: 4, 28 | decoration: BoxDecoration( 29 | borderRadius: BorderRadius.circular(45), 30 | color: Get.theme.hintColor.withOpacity(0.2), 31 | ), 32 | ), 33 | ), 34 | const SizedBox(height: 8), 35 | child, 36 | ], 37 | ), 38 | ), 39 | ), 40 | backgroundColor: Get.theme.colorScheme.background, 41 | isScrollControlled: isScrollControlled, 42 | elevation: 0, 43 | ); 44 | } 45 | 46 | // show animated bottomsheet 47 | showAnimatedBottomSheet() { 48 | return BottomSheet( 49 | onClosing: () {}, 50 | backgroundColor: Colors.red, 51 | constraints: BoxConstraints( 52 | maxHeight: 200, 53 | minHeight: 200, 54 | minWidth: Get.width, 55 | maxWidth: Get.width, 56 | ), 57 | builder: (context) => Container( 58 | height: 200, 59 | width: 200, 60 | color: Colors.red, 61 | child: const Center( 62 | child: Text("Test"), 63 | )), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/profile/data/remote/data_sources/profile_remote_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | import '../../../domain/entities/profile.dart'; 4 | 5 | abstract class ProfileRemoteDatabase { 6 | /// Saves the [Profile] to the remote database 7 | Future save(Profile profile); 8 | 9 | /// Retrieves the [Profile] from the remote database 10 | Future retrieve(String id); 11 | 12 | /// Deletes the [Profile] from the remote database 13 | Future delete(String id); 14 | 15 | /// Updates the [Profile] to the remote database 16 | Future update(Profile profile); 17 | 18 | /// Checks if the [Profile] exists in the remote database 19 | Future exists(String id); 20 | } 21 | 22 | class ProfileRemoteDatabaseImpl implements ProfileRemoteDatabase { 23 | @override 24 | Future save(Profile profile) async { 25 | await FirebaseFirestore.instance 26 | .collection('profiles') 27 | .doc(profile.id) 28 | .set(profile.toJson(), SetOptions(merge: true)); 29 | } 30 | 31 | @override 32 | Future retrieve(String id) async { 33 | final results = await FirebaseFirestore.instance.collection('profiles').doc(id).get(); 34 | return Profile.fromJson(results.data()!); 35 | } 36 | 37 | @override 38 | Future delete(String id) async { 39 | await FirebaseFirestore.instance.collection('profiles').doc(id).delete(); 40 | } 41 | 42 | @override 43 | Future update(Profile profile) async { 44 | await FirebaseFirestore.instance 45 | .collection('profiles') 46 | .doc(profile.id) 47 | .set(profile.toJson(), SetOptions(merge: true)); 48 | return profile; 49 | } 50 | 51 | @override 52 | Future exists(String id) async { 53 | final results = await FirebaseFirestore.instance.collection('profiles').doc(id).get(); 54 | return results.exists; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/message.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:studyhive/src/hive/domain/entities/media.dart'; 3 | import 'package:studyhive/src/hive/domain/entities/topic.dart'; 4 | 5 | part 'message.freezed.dart'; 6 | part 'message.g.dart'; 7 | 8 | @Freezed(makeCollectionsUnmodifiable: false) 9 | class Message with _$Message { 10 | const factory Message({ 11 | /// Unique ID of the message 12 | required String id, 13 | 14 | /// ID of the user who sent the message 15 | required String senderId, 16 | 17 | /// Text of the message 18 | String? text, 19 | 20 | /// Media attached to the message 21 | @Default([]) List media, 22 | 23 | /// Question type if the message is a question 24 | QuestionType? questionType, 25 | 26 | /// Topic object if the message is a question or a material 27 | Topic? topic, 28 | 29 | /// List of options if the message is a poll 30 | @Default([]) List options, 31 | 32 | /// The date the message was sent 33 | required DateTime sentAt, 34 | 35 | /// The type of the message 36 | required MessageType type, 37 | }) = _Message; 38 | 39 | factory Message.fromJson(Map json) => _$MessageFromJson(json); 40 | } 41 | 42 | enum MessageType { 43 | discussion, 44 | question, 45 | announcement, 46 | poll, 47 | material, 48 | text, 49 | } 50 | 51 | enum QuestionType { 52 | multipleChoice, 53 | trueFalse, 54 | shortAnswer, 55 | longAnswer, 56 | } 57 | 58 | extension QuestionTypeX on QuestionType { 59 | String get text { 60 | switch (this) { 61 | case QuestionType.multipleChoice: 62 | return "Multiple Choice"; 63 | case QuestionType.trueFalse: 64 | return "True or False"; 65 | case QuestionType.shortAnswer: 66 | return "Short Answer"; 67 | case QuestionType.longAnswer: 68 | return "Long Answer"; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/shared/utils/pick_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_cropper/image_cropper.dart'; 3 | import 'package:image_picker/image_picker.dart'; 4 | 5 | Future pickImage({ 6 | ImageSource source = ImageSource.gallery, 7 | double? maxWidth, 8 | double? maxHeight, 9 | int? quality = 90, 10 | }) async { 11 | try { 12 | final pickedImage = 13 | await ImagePicker().pickImage(source: source, maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality); 14 | return pickedImage; 15 | } on Exception catch (e) { 16 | return null; 17 | } 18 | } 19 | 20 | Future pickVideo({ 21 | ImageSource source = ImageSource.gallery, 22 | Duration maxDuration = const Duration(minutes: 3), 23 | }) async { 24 | try { 25 | final pickedVideo = await ImagePicker().pickVideo(source: source, maxDuration: maxDuration); 26 | return pickedVideo; 27 | } on Exception catch (e) { 28 | return null; 29 | } 30 | } 31 | 32 | Future cropImage({ 33 | required String imagePath, 34 | String? title, 35 | }) async { 36 | try { 37 | final croppedImage = await ImageCropper().cropImage( 38 | sourcePath: imagePath, 39 | // make it circular 40 | aspectRatioPresets: [ 41 | CropAspectRatioPreset.square, 42 | CropAspectRatioPreset.ratio3x2, 43 | CropAspectRatioPreset.original, 44 | CropAspectRatioPreset.ratio4x3, 45 | ], 46 | uiSettings: [ 47 | AndroidUiSettings( 48 | toolbarTitle: title ?? '', 49 | toolbarColor: Colors.transparent, 50 | initAspectRatio: CropAspectRatioPreset.original, 51 | lockAspectRatio: false, 52 | ), 53 | IOSUiSettings( 54 | title: title ?? '', 55 | aspectRatioLockEnabled: false, 56 | showCancelConfirmationDialog: true, 57 | ) 58 | ], 59 | ); 60 | return croppedImage; 61 | } on Exception catch (e) { 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/routes/app_pages.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:studyhive/src/hive/presentation/bindings/create_binding.dart'; 3 | import 'package:studyhive/src/hive/presentation/bindings/hive_binding.dart'; 4 | import 'package:studyhive/src/hive/presentation/pages/create.dart'; 5 | import 'package:studyhive/src/hive/presentation/pages/hive.dart'; 6 | import 'package:studyhive/src/profile/presentation/manager/profile_binding.dart'; 7 | import 'package:studyhive/src/profile/presentation/pages/profile.dart'; 8 | import 'package:studyhive/src/profile/presentation/pages/setup.dart'; 9 | import 'package:studyhive/src/settings/presentation/manager/settings_binding.dart'; 10 | import 'package:studyhive/src/settings/presentation/pages/settings.dart'; 11 | 12 | import '../src/auth/presentation/manager/auth_binding.dart'; 13 | import '../src/auth/presentation/pages/phone_auth.dart'; 14 | import '../src/home/presentation/manager/home_binding.dart'; 15 | import '../src/home/presentation/pages/home.dart'; 16 | import '../src/onboarding/presentation/manager/onboarding_binding.dart'; 17 | import '../src/onboarding/presentation/pages/onboarding.dart'; 18 | 19 | part './app_routes.dart'; 20 | 21 | class RouteGet { 22 | static final List getPages = [ 23 | GetPage(name: AppRoutes.home, page: () => const HomePage(), binding: HomeBinding()), 24 | GetPage(name: AppRoutes.onboarding, page: () => const OnboardingPage(), binding: OnboardingBinding()), 25 | GetPage(name: AppRoutes.phoneAuth, page: () => const PhoneAuthPage(), binding: AuthBinding()), 26 | GetPage(name: AppRoutes.setupProfile, page: () => const SetupProfilePage(), binding: ProfileBinding()), 27 | GetPage(name: AppRoutes.profile, page: () => const ProfilePage(), binding: ProfileBinding()), 28 | GetPage(name: AppRoutes.createHive, page: () => const CreateHivePage(), binding: CreateHiveBinding()), 29 | GetPage(name: AppRoutes.settings, page: () => const SettingsPage(), binding: SettingsBinding()), 30 | GetPage(name: AppRoutes.hive, page: () => const HivePage(), binding: HiveBinding()), 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/message.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'message.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Message _$$_MessageFromJson(Map json) => _$_Message( 10 | id: json['id'] as String, 11 | senderId: json['senderId'] as String, 12 | text: json['text'] as String?, 13 | media: (json['media'] as List?) 14 | ?.map((e) => Media.fromJson(e as Map)) 15 | .toList() ?? 16 | const [], 17 | questionType: 18 | $enumDecodeNullable(_$QuestionTypeEnumMap, json['questionType']), 19 | topic: json['topic'] == null 20 | ? null 21 | : Topic.fromJson(json['topic'] as Map), 22 | options: (json['options'] as List?) 23 | ?.map((e) => e as String) 24 | .toList() ?? 25 | const [], 26 | sentAt: DateTime.parse(json['sentAt'] as String), 27 | type: $enumDecode(_$MessageTypeEnumMap, json['type']), 28 | ); 29 | 30 | Map _$$_MessageToJson(_$_Message instance) => 31 | { 32 | 'id': instance.id, 33 | 'senderId': instance.senderId, 34 | 'text': instance.text, 35 | 'media': instance.media, 36 | 'questionType': _$QuestionTypeEnumMap[instance.questionType], 37 | 'topic': instance.topic, 38 | 'options': instance.options, 39 | 'sentAt': instance.sentAt.toIso8601String(), 40 | 'type': _$MessageTypeEnumMap[instance.type]!, 41 | }; 42 | 43 | const _$QuestionTypeEnumMap = { 44 | QuestionType.multipleChoice: 'multipleChoice', 45 | QuestionType.trueFalse: 'trueFalse', 46 | QuestionType.shortAnswer: 'shortAnswer', 47 | QuestionType.longAnswer: 'longAnswer', 48 | }; 49 | 50 | const _$MessageTypeEnumMap = { 51 | MessageType.discussion: 'discussion', 52 | MessageType.question: 'question', 53 | MessageType.announcement: 'announcement', 54 | MessageType.poll: 'poll', 55 | MessageType.material: 'material', 56 | MessageType.text: 'text', 57 | }; 58 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/profile/presentation/pages/setup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:studyhive/shared/extensions/buttons.dart'; 4 | import 'package:studyhive/shared/theme/theme.dart'; 5 | import 'package:studyhive/src/profile/presentation/manager/setup_controller.dart'; 6 | 7 | class SetupProfilePage extends GetView { 8 | const SetupProfilePage({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | body: SafeArea( 14 | child: Padding( 15 | padding: const EdgeInsets.only(left: 18, right: 18, top: 48, bottom: 18), 16 | child: Column( 17 | crossAxisAlignment: CrossAxisAlignment.start, 18 | children: [ 19 | Text( 20 | 'what_is_your_name'.tr, 21 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 22 | fontWeight: FontWeight.bold, 23 | fontSize: 20, 24 | ), 25 | ), 26 | Padding( 27 | padding: const EdgeInsets.only(top: 6.0, bottom: 30), 28 | child: Text( 29 | 'we_will_use_this_for_your_profile'.tr, 30 | style: Theme.of(context).textTheme.bodyMedium, 31 | ), 32 | ), 33 | Obx(() { 34 | return TextFormField( 35 | autofocus: true, 36 | controller: controller.nameController, 37 | keyboardType: TextInputType.name, 38 | enabled: !controller.loading.value, 39 | maxLength: 20, 40 | decoration: InputDecoration( 41 | hintText: 'full_name'.tr, 42 | contentPadding: inputPadding, 43 | ), 44 | ); 45 | }), 46 | const Spacer(), 47 | SizedBox( 48 | width: double.maxFinite, 49 | child: Obx( 50 | () { 51 | return ElevatedButton( 52 | onPressed: controller.enableFinishButton.value ? controller.updateProfile : null, 53 | child: Text( 54 | 'continue'.tr, 55 | ), 56 | ).withLoading(loading: controller.loading); 57 | }, 58 | ), 59 | ) 60 | ], 61 | ), 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/shared/validation/validator.dart: -------------------------------------------------------------------------------- 1 | /// Validate user input and data 2 | class Validator { 3 | /// Validates Full Name and needs to be more than 4 characters 4 | static String? name(String? value) { 5 | const pattern = r'(^[a-zA-Z ]*$)'; 6 | final regExp = RegExp(pattern); 7 | if (regExp.hasMatch(value!) && value.trim().length > 4) { 8 | return null; 9 | } else if (value.trim().length < 5) { 10 | return 'Full name must be more than 4 characters'; 11 | } 12 | return null; 13 | } 14 | 15 | /// Pattern checks for valid phone Numbers 16 | // static String? phoneNumber(String? value) { 17 | // const pattern = r'(^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$)'; 18 | // final regExp = RegExp(pattern); 19 | // if (value!.isEmpty) { 20 | // return '😉 Come on, don\'t be shy, enter your number'; 21 | // } else if (!regExp.hasMatch(value)) { 22 | // return "Hmm 🤔, that doesn't look like a valid phone number"; 23 | // } else if (value.length > 16 || value.length < 9) { 24 | // return "Hmm 🤔, that doesn't look like a valid phone number"; 25 | // } else if (value.startsWith('0') && value.length < 10) { 26 | // return "👏 Come on, ${10 - value.length} digits more"; 27 | // } else if (value.startsWith('0') && value.length > 10) { 28 | // return "📢 Valid phone numbers are 10 digits right ?"; 29 | // } else if (value.startsWith("") && value.length < 12) { 30 | // return "Not sure if this is a valid phone number"; 31 | // } 32 | // return null; 33 | // } 34 | static String? phoneNumber(String? value) { 35 | if (value!.isEmpty) { 36 | return '😉 Come on, don\'t be shy, enter your number'; 37 | } else if (value.length < 10) { 38 | return '👏 Come on, ${10 - value.length} digits more'; 39 | } 40 | 41 | /// When [value] is greater than 10 42 | else if (value.length > 10) { 43 | return '📢 Valid phone numbers are 10 digits right ?'; 44 | } 45 | return null; 46 | } 47 | 48 | /// pattern for a valid code must start with either grp_ or comm_ and the length is 10 plus the prefix 49 | 50 | static String? validCode(String? value) { 51 | if (value!.isEmpty) { 52 | return "Come on, enter the code"; 53 | } else if (!value.startsWith("grp_") && !value.startsWith("comm_")) { 54 | return "Invalid community or group code"; 55 | } else if (value.startsWith('grp_') && value.length != 14) { 56 | return "Invalid group code"; 57 | } else if (value.startsWith('comm_') && value.length != 15) { 58 | return "Invalid Community code"; 59 | } 60 | 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | // START: FlutterFire Configuration 26 | apply plugin: 'com.google.gms.google-services' 27 | // END: FlutterFire Configuration 28 | apply plugin: 'kotlin-android' 29 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 30 | 31 | android { 32 | compileSdkVersion flutter.compileSdkVersion 33 | ndkVersion flutter.ndkVersion 34 | 35 | compileOptions { 36 | sourceCompatibility JavaVersion.VERSION_1_8 37 | targetCompatibility JavaVersion.VERSION_1_8 38 | } 39 | 40 | kotlinOptions { 41 | jvmTarget = '1.8' 42 | } 43 | 44 | sourceSets { 45 | main.java.srcDirs += 'src/main/kotlin' 46 | } 47 | 48 | defaultConfig { 49 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 50 | applicationId "com.jerondev.studyhive.studyhive" 51 | // You can update the following values to match your application needs. 52 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 53 | minSdkVersion 21 54 | targetSdkVersion flutter.targetSdkVersion 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | } 58 | 59 | buildTypes { 60 | release { 61 | // TODO: Add your own signing config for the release build. 62 | // Signing with the debug keys for now, so `flutter run --release` works. 63 | signingConfig signingConfigs.debug 64 | } 65 | } 66 | } 67 | 68 | flutter { 69 | source '../..' 70 | } 71 | 72 | dependencies { 73 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 74 | 75 | } 76 | apply plugin: 'com.google.firebase.crashlytics' -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:get/get.dart'; 7 | import 'package:studyhive/routes/app_pages.dart'; 8 | import 'package:studyhive/services/init_services.dart'; 9 | import 'package:studyhive/shared/theme/theme.dart'; 10 | import 'package:studyhive/src/auth/presentation/manager/auth_binding.dart'; 11 | import 'package:studyhive/src/home/presentation/manager/home_binding.dart'; 12 | import 'package:studyhive/src/profile/data/local/data_sources/profile_local_database.dart'; 13 | import 'package:studyhive/translations/translation.dart'; 14 | 15 | import 'firebase_options.dart'; 16 | 17 | void main() async { 18 | await initServices(); 19 | runZonedGuarded(() async { 20 | WidgetsFlutterBinding.ensureInitialized(); 21 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 22 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; 23 | final profileLocalDb = Get.find(); 24 | final bool isAuthenticated = await profileLocalDb.authStatus(); 25 | final bool isProfileSetup = await profileLocalDb.finishedSetup(); 26 | runApp(MyApp( 27 | isAuthenticated: isAuthenticated, 28 | isProfileSetup: isProfileSetup, 29 | )); 30 | }, (error, stack) { 31 | print('runZonedGuarded: Caught error in my root zone. $error'); 32 | FirebaseCrashlytics.instance.recordError(error, stack); 33 | }); 34 | } 35 | 36 | class MyApp extends StatelessWidget { 37 | const MyApp({super.key, required this.isAuthenticated, required this.isProfileSetup}); 38 | 39 | final bool isAuthenticated; 40 | final bool isProfileSetup; 41 | 42 | String get initialRoute { 43 | if (isAuthenticated) { 44 | if (isProfileSetup) { 45 | return AppRoutes.home; 46 | } else { 47 | return AppRoutes.setupProfile; 48 | } 49 | } else { 50 | return AppRoutes.onboarding; 51 | } 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return GetMaterialApp( 57 | useInheritedMediaQuery: true, 58 | locale: Get.deviceLocale, 59 | translations: Localization(), 60 | title: 'Study Hive', 61 | theme: lightTheme, 62 | darkTheme: darkTheme, 63 | debugShowCheckedModeBanner: false, 64 | initialBinding: isAuthenticated ? HomeBinding() : AuthBinding(), 65 | initialRoute: initialRoute, 66 | getPages: RouteGet.getPages, 67 | ); 68 | } 69 | } 70 | 71 | // flutter pub run build_runner build --delete-conflicting-outputs 72 | -------------------------------------------------------------------------------- /lib/src/profile/data/repositories/profile_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:studyhive/shared/network/network.dart'; 4 | 5 | import '../../../../shared/error/failure.dart'; 6 | import '../../domain/entities/profile.dart'; 7 | import '../../domain/repositories/profile_repository.dart'; 8 | import '../local/data_sources/profile_local_database.dart'; 9 | import '../remote/data_sources/profile_remote_database.dart'; 10 | 11 | class ProfileRepositoryImpl implements ProfileRepository { 12 | final ProfileLocalDatabase localDatabase; 13 | final ProfileRemoteDatabase remoteDatabase; 14 | final NetworkInfo networkInfo; 15 | 16 | const ProfileRepositoryImpl({ 17 | required this.localDatabase, 18 | required this.remoteDatabase, 19 | required this.networkInfo, 20 | }); 21 | 22 | @override 23 | Future> retrieve(String userId) async { 24 | try { 25 | if (!await networkInfo.hasInternet()) { 26 | final localProfile = await localDatabase.retrieve(); 27 | if (localProfile != Profile.empty()) { 28 | return Right(localProfile); 29 | } 30 | return Left(Failure("no_internet".tr)); 31 | } 32 | final remoteProfile = await remoteDatabase.retrieve(userId); 33 | await localDatabase.save(remoteProfile); 34 | return Right(remoteProfile); 35 | } on Exception { 36 | return const Left(Failure('Error retrieving profile')); 37 | } 38 | } 39 | 40 | @override 41 | Future> save(Profile profile) async { 42 | try { 43 | await localDatabase.save(profile); 44 | await remoteDatabase.save(profile); 45 | return const Right(null); 46 | } on Exception { 47 | return const Left(Failure('Error saving profile')); 48 | } 49 | } 50 | 51 | @override 52 | Future> delete(String userId) async { 53 | try { 54 | if (!await networkInfo.hasInternet()) { 55 | return Left(Failure("no_internet".tr)); 56 | } 57 | await localDatabase.delete(); 58 | await remoteDatabase.delete(userId); 59 | return const Right(null); 60 | } on Exception { 61 | return const Left(Failure('Error deleting profile')); 62 | } 63 | } 64 | 65 | @override 66 | Future> update(Profile profile) async { 67 | try { 68 | if (!await networkInfo.hasInternet()) { 69 | return Left(Failure('no_internet'.tr)); 70 | } 71 | await localDatabase.save(profile); 72 | final results = await remoteDatabase.update(profile); 73 | return Right(results); 74 | } on Exception { 75 | return const Left(Failure('Error updating profile')); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/manager/hive_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:nanoid/nanoid.dart'; 5 | import 'package:studyhive/src/hive/domain/entities/message.dart'; 6 | import 'package:studyhive/src/hive/domain/use_cases/details.dart'; 7 | import 'package:studyhive/src/hive/domain/use_cases/post_message.dart'; 8 | 9 | import '../../../../shared/ui/snackbars.dart'; 10 | import '../../../../shared/usecase/usecase.dart'; 11 | import '../../../profile/domain/entities/profile.dart'; 12 | import '../../../profile/domain/use_cases/retrieve.dart'; 13 | import '../../domain/entities/hive.dart'; 14 | 15 | class HiveController extends GetxController { 16 | final String id = Get.parameters['id'] ?? ''; 17 | final Hive hive = Get.arguments['hive'] ?? Hive.empty(); 18 | final hiveDetails = Get.find(); 19 | final retrieveProfile = Get.find(); 20 | final postMessage = Get.find(); 21 | final textController = TextEditingController(); 22 | Profile _profile = Profile.empty(); 23 | RxBool retrievingProfile = false.obs; 24 | RxBool showSendButton = false.obs; 25 | 26 | RxInt activePageIndex = 0.obs; 27 | 28 | Stream details() async* { 29 | final result = await hiveDetails(Params(id)); 30 | yield* result.fold((failure) async* { 31 | showErrorSnackbar(message: failure.message); 32 | yield Hive.empty(); 33 | }, (hive) async* { 34 | yield* hive; 35 | }); 36 | } 37 | 38 | Future _retrieveProfile() async { 39 | retrievingProfile.value = true; 40 | final result = await retrieveProfile(Params(FirebaseAuth.instance.currentUser!.uid)); 41 | return result.fold((failure) { 42 | retrievingProfile.value = false; 43 | showErrorSnackbar(message: failure.message); 44 | }, (profile) { 45 | _profile = profile; 46 | retrievingProfile.value = false; 47 | }); 48 | } 49 | 50 | Future sendMessage() async { 51 | final message = Message( 52 | id: nanoid(), 53 | senderId: _profile.id, 54 | sentAt: DateTime.now(), 55 | type: MessageType.text, 56 | text: textController.text.trim(), 57 | ); 58 | final result = await postMessage(Params(PostMessageParams( 59 | id, 60 | message, 61 | ))); 62 | result.fold((failure) { 63 | showErrorSnackbar(message: failure.message); 64 | }, (messageId) { 65 | textController.clear(); 66 | }); 67 | } 68 | 69 | @override 70 | void onInit() { 71 | _retrieveProfile(); 72 | textController.addListener(() { 73 | showSendButton.value = textController.text.trim().isNotEmpty; 74 | }); 75 | super.onInit(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyA80i33kNWH1kJ59S6orqaH4Jhps9hTXhc', 54 | appId: '1:480976755846:android:ef9886b15773dc76c40507', 55 | messagingSenderId: '480976755846', 56 | projectId: 'studyhive-og', 57 | storageBucket: 'studyhive-og.appspot.com', 58 | ); 59 | 60 | static const FirebaseOptions ios = FirebaseOptions( 61 | apiKey: 'AIzaSyCGZMjju3aukPvqzqPaBd6dEftlv0opWmU', 62 | appId: '1:480976755846:ios:1c8abd08c81a5382c40507', 63 | messagingSenderId: '480976755846', 64 | projectId: 'studyhive-og', 65 | storageBucket: 'studyhive-og.appspot.com', 66 | iosClientId: '480976755846-vbej0t2b5kvbbh3vf047sfk4028hausd.apps.googleusercontent.com', 67 | iosBundleId: 'com.jerondev.studyhive.studyhive', 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/new_material.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:iconly/iconly.dart'; 3 | 4 | import '../../../../shared/theme/theme.dart'; 5 | 6 | class NewMaterial extends StatelessWidget { 7 | const NewMaterial({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | leading: IconButton( 14 | splashRadius: 20, 15 | onPressed: () { 16 | Navigator.pop(context); 17 | }, 18 | icon: const Icon(Icons.close), 19 | ), 20 | actions: [ 21 | Padding( 22 | padding: const EdgeInsets.only(right: 15.0), 23 | child: ElevatedButton(onPressed: () {}, child: const Text("Share")), 24 | ), 25 | ], 26 | ), 27 | body: ListView( 28 | padding: const EdgeInsets.all(14), 29 | children: [ 30 | Padding( 31 | padding: const EdgeInsets.only(bottom: 8.0), 32 | child: TextFormField( 33 | keyboardType: TextInputType.multiline, 34 | maxLines: null, 35 | maxLength: 100, 36 | minLines: 1, 37 | decoration: const InputDecoration( 38 | contentPadding: inputPadding, 39 | hintText: "Describe your material...", 40 | ), 41 | ), 42 | ), 43 | Padding( 44 | padding: const EdgeInsets.only(bottom: 8.0), 45 | child: ListTile( 46 | onTap: () {}, 47 | title: Text("Not Set", 48 | style: Theme.of(context).textTheme.bodySmall!.copyWith( 49 | fontWeight: FontWeight.bold, 50 | )), 51 | subtitle: Text( 52 | "Topic", 53 | style: Theme.of(context).textTheme.bodySmall, 54 | ), 55 | trailing: const Icon( 56 | IconlyLight.arrow_down_2, 57 | size: 17, 58 | ), 59 | ), 60 | ), 61 | Padding( 62 | padding: const EdgeInsets.only(left: 5), 63 | child: Wrap( 64 | spacing: -5, 65 | children: [ 66 | IconButton( 67 | onPressed: () {}, 68 | icon: const Icon(IconlyLight.image), 69 | iconSize: 20, 70 | splashRadius: 16, 71 | ), 72 | IconButton( 73 | onPressed: () {}, 74 | icon: const Icon(IconlyLight.video), 75 | iconSize: 20, 76 | ), 77 | IconButton( 78 | onPressed: () {}, 79 | icon: const Icon(IconlyLight.document), 80 | iconSize: 20, 81 | ), 82 | ], 83 | ), 84 | ) 85 | ], 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | 13 | 14 | com.googleusercontent.apps.480976755846-vbej0t2b5kvbbh3vf047sfk4028hausd 15 | 16 | 17 | 18 | CFBundleDevelopmentRegion 19 | $(DEVELOPMENT_LANGUAGE) 20 | CFBundleDisplayName 21 | Studyhive 22 | CFBundleExecutable 23 | $(EXECUTABLE_NAME) 24 | CFBundleIdentifier 25 | $(PRODUCT_BUNDLE_IDENTIFIER) 26 | CFBundleInfoDictionaryVersion 27 | 6.0 28 | CFBundleName 29 | studyhive 30 | CFBundlePackageType 31 | APPL 32 | CFBundleShortVersionString 33 | $(FLUTTER_BUILD_NAME) 34 | CFBundleSignature 35 | ???? 36 | CFBundleVersion 37 | $(FLUTTER_BUILD_NUMBER) 38 | LSRequiresIPhoneOS 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIMainStoryboardFile 43 | Main 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | UIViewControllerBasedStatusBarAppearance 58 | 59 | CADisableMinimumFrameDurationOnPhone 60 | 61 | UIApplicationSupportsIndirectInputEvents 62 | 63 | NSCameraUsageDescription 64 | This app needs to access your camera to allow you to record videos and take photos for your profile. Without this permission, you won't be able to use these features. 65 | NSPhotoLibraryUsageDescription 66 | This app needs to access your photo library to allow you to update your profile picture and upload photos. Without this permission, you won't be able to use these features 67 | NSMicrophoneUsageDescription 68 | This app needs to access your microphone to allow you to record audio for your profile. Without this permission, you won't be able to use this feature 69 | 70 | 71 | -------------------------------------------------------------------------------- /lib/src/onboarding/presentation/pages/onboarding.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:ionicons/ionicons.dart'; 5 | import 'package:studyhive/generated/assets.dart'; 6 | import 'package:studyhive/routes/app_pages.dart'; 7 | import 'package:studyhive/shared/extensions/buttons.dart'; 8 | 9 | import '../manager/onboarding_controller.dart'; 10 | 11 | class OnboardingPage extends GetView { 12 | const OnboardingPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | body: SafeArea( 18 | child: Padding( 19 | padding: const EdgeInsets.all(18), 20 | child: Center( 21 | child: Column( 22 | children: [ 23 | const Spacer(), 24 | ConstrainedBox( 25 | constraints: const BoxConstraints(maxWidth: 400), 26 | child: Image.asset( 27 | Assets.onboarding, 28 | ), 29 | ), 30 | const Spacer(), 31 | Text( 32 | 'welcome'.tr, 33 | style: Theme.of(context).textTheme.titleLarge!.apply( 34 | fontWeightDelta: 2, 35 | ), 36 | textAlign: TextAlign.center, 37 | ), 38 | Padding( 39 | padding: const EdgeInsets.only(top: 10, bottom: 30), 40 | child: Text( 41 | "welcome_sub_text".tr, 42 | style: Theme.of(context).textTheme.bodyMedium, 43 | textAlign: TextAlign.center, 44 | ), 45 | ), 46 | Padding( 47 | padding: const EdgeInsets.only(bottom: 12), 48 | child: SizedBox( 49 | width: double.infinity, 50 | child: ElevatedButton.icon( 51 | onPressed: controller.signInWithGoogle, 52 | label: Text('continue_with_google'.tr), 53 | icon: const Icon(Ionicons.logo_google), 54 | ).withLoading( 55 | loading: controller.loading, 56 | text: 'signing_in'.tr, 57 | icon: const Icon(Ionicons.logo_google), 58 | ), 59 | ), 60 | ), 61 | SizedBox( 62 | width: double.infinity, 63 | child: ElevatedButton.icon( 64 | onPressed: () { 65 | Get.toNamed(AppRoutes.phoneAuth); 66 | }, 67 | style: ElevatedButton.styleFrom( 68 | foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer, 69 | backgroundColor: Theme.of(context).colorScheme.secondaryContainer, 70 | ), 71 | label: Text('continue_with_phone'.tr), 72 | icon: const Icon(IconlyBold.call), 73 | ), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ), 79 | )); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/manager/create_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:nanoid/nanoid.dart'; 5 | import 'package:studyhive/shared/ui/snackbars.dart'; 6 | import 'package:studyhive/shared/usecase/usecase.dart'; 7 | import 'package:studyhive/shared/utils/upload_image.dart'; 8 | import 'package:studyhive/src/hive/domain/entities/hive.dart'; 9 | import 'package:studyhive/src/hive/domain/use_cases/create.dart'; 10 | import 'package:studyhive/src/profile/domain/use_cases/retrieve.dart'; 11 | 12 | import '../../../../shared/utils/pick_image.dart'; 13 | import '../../../profile/domain/entities/profile.dart'; 14 | 15 | class CreateHiveController extends GetxController { 16 | CreateHiveController(this.createHive); 17 | 18 | final nameController = TextEditingController(); 19 | final CreateHive createHive; 20 | final profileUseCase = Get.find(); 21 | RxBool enableButton = false.obs; 22 | RxBool loading = false.obs; 23 | RxString hiveProfile = ''.obs; 24 | 25 | @override 26 | void onInit() { 27 | super.onInit(); 28 | nameController.addListener(() { 29 | enableButton.value = nameController.text.isNotEmpty; 30 | }); 31 | } 32 | 33 | Future chooseHiveProfile() async { 34 | final pickedImage = await pickImage(maxHeight: 512, maxWidth: 512); 35 | if (pickedImage != null) { 36 | final croppedImage = await cropImage(imagePath: pickedImage.path, title: 'Choose Hive Profile'); 37 | if (croppedImage != null) { 38 | hiveProfile.value = croppedImage.path; 39 | } 40 | } 41 | } 42 | 43 | Future _retrieveProfile() async { 44 | final result = await profileUseCase(Params(FirebaseAuth.instance.currentUser!.uid)); 45 | return result.fold( 46 | (failure) => Profile.empty(), 47 | (profile) => profile, 48 | ); 49 | } 50 | 51 | Future create() async { 52 | loading.value = true; 53 | final profile = await _retrieveProfile(); 54 | 55 | late String? downloadUrl; 56 | if (hiveProfile.value.isNotEmpty) { 57 | try { 58 | downloadUrl = await uploadImage(imagePath: hiveProfile.value, folder: "hive_profiles"); 59 | } on Exception { 60 | showErrorSnackbar(message: "Check your connectivity and try again"); 61 | } 62 | } else { 63 | downloadUrl = null; 64 | } 65 | 66 | final hive = Hive( 67 | id: nanoid(), 68 | name: nameController.text, 69 | createdBy: profile.id, 70 | createdAt: DateTime.now(), 71 | updatedAt: DateTime.now(), 72 | members: [profile.id], 73 | admins: [profile.id], 74 | photoUrl: downloadUrl, 75 | conversations: [], 76 | ); 77 | final result = await createHive(Params(hive)); 78 | 79 | result.fold( 80 | (failure) { 81 | showErrorSnackbar(message: failure.message); 82 | loading.value = false; 83 | }, 84 | (hiveId) { 85 | loading.value = false; 86 | Get.offNamedUntil('/hive/$hiveId', (route) => route.isFirst, arguments: { 87 | "hive": hive, 88 | "tab": 2, 89 | }); 90 | showSuccessSnackbar(message: "Hive created successfully"); 91 | }, 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/settings/presentation/manager/settings_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:image_picker/image_picker.dart'; 4 | import 'package:studyhive/routes/app_pages.dart'; 5 | import 'package:studyhive/shared/ui/snackbars.dart'; 6 | import 'package:studyhive/src/profile/domain/entities/profile.dart'; 7 | import 'package:studyhive/src/profile/domain/use_cases/update.dart'; 8 | 9 | import '../../../../shared/usecase/usecase.dart'; 10 | import '../../../../shared/utils/pick_image.dart'; 11 | import '../../../../shared/utils/upload_image.dart'; 12 | import '../../../auth/domain/repositories/auth_repository.dart'; 13 | import '../../../profile/domain/use_cases/retrieve.dart'; 14 | 15 | class SettingsController extends GetxController with StateMixin { 16 | final retrieveProfileUseCase = Get.find(); 17 | final updateProfileUseCase = Get.find(); 18 | final authRepositoryUseCase = Get.find(); 19 | Profile _profile = Profile.empty(); 20 | RxBool uploading = false.obs; 21 | 22 | @override 23 | void onInit() async { 24 | super.onInit(); 25 | await _retrieveProfile(); 26 | } 27 | 28 | Future _retrieveProfile() async { 29 | final result = await retrieveProfileUseCase(Params(FirebaseAuth.instance.currentUser!.uid)); 30 | return result.fold( 31 | (failure) => change(Profile.empty(), status: RxStatus.error("There was an error retrieving your profile")), 32 | (profile) { 33 | _profile = profile; 34 | change(profile, status: RxStatus.success()); 35 | }); 36 | } 37 | 38 | Future _updateProfile(Profile profile) async { 39 | final result = await updateProfileUseCase(Params(profile)); 40 | return result.fold((failure) => showErrorSnackbar(message: failure.message), (profile) { 41 | _profile = profile; 42 | change(profile, status: RxStatus.success()); 43 | }); 44 | } 45 | 46 | Future chooseProfile(ImageSource source) async { 47 | final pickedImage = await pickImage(maxHeight: 512, maxWidth: 512, source: source); 48 | if (pickedImage != null) { 49 | final croppedImage = await cropImage(imagePath: pickedImage.path, title: 'Choose Profile'); 50 | if (croppedImage != null) { 51 | uploading.value = true; 52 | final downloadUrl = await uploadImage(imagePath: croppedImage.path, folder: "user_profiles"); 53 | if (downloadUrl != null) { 54 | _profile = _profile.copyWith(photoUrl: downloadUrl); 55 | await _updateProfile(_profile); 56 | uploading.value = false; 57 | } else { 58 | showErrorSnackbar(message: "There was an error updating your profile picture"); 59 | uploading.value = false; 60 | } 61 | } 62 | } else {} 63 | } 64 | 65 | Future deleteProfile() async { 66 | uploading.value = true; 67 | _profile = _profile.copyWith(photoUrl: null); 68 | await _updateProfile(_profile); 69 | uploading.value = false; 70 | } 71 | 72 | Future signOut() async { 73 | final results = await authRepositoryUseCase.signOut(); 74 | results.fold( 75 | (failure) => showErrorSnackbar(message: failure.message), (unit) => Get.offAllNamed(AppRoutes.onboarding)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/hive/data/remote/data_sources/hive_remote_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:studyhive/src/profile/data/remote/data_sources/profile_remote_database.dart'; 3 | 4 | import '../../../domain/entities/hive.dart'; 5 | import '../../../domain/entities/message.dart'; 6 | 7 | abstract class HiveRemoteDatabase { 8 | /// Returns a list of all the [Hive]s 9 | Stream> list(String userId); 10 | 11 | /// Creates a new [Hive] 12 | Future create(Hive hive); 13 | 14 | /// Updates an existing [Hive] 15 | Future update(Hive hive); 16 | 17 | /// Deletes an existing [Hive] 18 | Future delete(String hiveId); 19 | 20 | /// Joins an existing [Hive] 21 | Future join({required String hiveId, required String userId}); 22 | 23 | /// Leaves an existing [Hive] 24 | Future leave({required String hiveId, required String userId}); 25 | 26 | /// Returns details of a [Hive] 27 | Stream details(String hiveId); 28 | 29 | /// Post a message to a [Hive] 30 | Future postMessage({required String hiveId, required Message message}); 31 | } 32 | 33 | class HiveRemoteDatabaseImpl implements HiveRemoteDatabase { 34 | final ProfileRemoteDatabase _profileRemoteDatabase; 35 | 36 | HiveRemoteDatabaseImpl(this._profileRemoteDatabase); 37 | 38 | @override 39 | Future create(Hive hive) async { 40 | await FirebaseFirestore.instance.collection('hives').doc(hive.id).set(hive.toJson()); 41 | return hive.id; 42 | } 43 | 44 | @override 45 | Future delete(String hiveId) async { 46 | await FirebaseFirestore.instance.collection('hives').doc(hiveId).delete(); 47 | return hiveId; 48 | } 49 | 50 | @override 51 | Future join({required String hiveId, required String userId}) async { 52 | await FirebaseFirestore.instance.collection('hives').doc(hiveId).update({ 53 | 'members': FieldValue.arrayUnion([userId]) 54 | }); 55 | return hiveId; 56 | } 57 | 58 | @override 59 | Stream> list(String userId) async* { 60 | yield* FirebaseFirestore.instance 61 | .collection('hives') 62 | .where('members', arrayContains: userId) 63 | .orderBy('name') 64 | .snapshots() 65 | .map((snapshot) => snapshot.docs.map((doc) => Hive.fromJson(doc.data())).toList()); 66 | } 67 | 68 | @override 69 | Future update(Hive hive) async { 70 | await FirebaseFirestore.instance.collection('hives').doc(hive.id).update(hive.toJson()); 71 | return hive.id; 72 | } 73 | 74 | @override 75 | Future leave({required String hiveId, required String userId}) async { 76 | await FirebaseFirestore.instance.collection('hives').doc(hiveId).update({ 77 | 'members': FieldValue.arrayRemove([userId]) 78 | }); 79 | return hiveId; 80 | } 81 | 82 | @override 83 | Stream details(String hiveId) async* { 84 | yield* FirebaseFirestore.instance 85 | .collection('hives') 86 | .doc(hiveId) 87 | .snapshots() 88 | .map((snapshot) => Hive.fromJson(snapshot.data()!)); 89 | } 90 | 91 | @override 92 | Future postMessage({required String hiveId, required Message message}) async { 93 | await FirebaseFirestore.instance.collection('hives').doc(hiveId).update({ 94 | 'conversations': FieldValue.arrayUnion([message.toJson()]) 95 | }); 96 | return hiveId; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/new_poll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:studyhive/src/hive/presentation/manager/poll_controller.dart'; 4 | 5 | import '../../../../shared/theme/theme.dart'; 6 | 7 | class NewPoll extends GetView { 8 | const NewPoll({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | appBar: AppBar( 14 | leading: IconButton( 15 | splashRadius: 20, 16 | onPressed: () { 17 | Navigator.pop(context); 18 | }, 19 | icon: const Icon(Icons.close), 20 | ), 21 | actions: [ 22 | Padding( 23 | padding: const EdgeInsets.only(right: 15.0), 24 | child: Obx(() { 25 | return ElevatedButton(onPressed: controller.canPost.value ? () {} : null, child: const Text("Post")); 26 | })), 27 | ], 28 | ), 29 | body: ListView( 30 | padding: const EdgeInsets.all(14), 31 | children: [ 32 | Padding( 33 | padding: const EdgeInsets.only(bottom: 8.0), 34 | child: TextFormField( 35 | controller: controller.pollTextController, 36 | keyboardType: TextInputType.multiline, 37 | maxLines: null, 38 | maxLength: 200, 39 | minLines: 1, 40 | decoration: const InputDecoration( 41 | contentPadding: inputPadding, 42 | hintText: "Ask your community...", 43 | ), 44 | ), 45 | ), 46 | // render the pollOptions down here 47 | Obx(() { 48 | return Column( 49 | children: controller.optionControllers 50 | .asMap() 51 | .entries 52 | .map( 53 | (entry) => Padding( 54 | padding: const EdgeInsets.only(bottom: 12.0), 55 | child: TextFormField( 56 | controller: entry.value, 57 | decoration: InputDecoration( 58 | contentPadding: inputPadding, 59 | hintText: "Option ${entry.key + 1}", 60 | suffixIcon: entry.key >= 2 61 | ? IconButton( 62 | onPressed: () { 63 | controller.optionControllers.removeAt(entry.key); 64 | entry.value.dispose(); 65 | }, 66 | icon: const Icon(Icons.close), 67 | iconSize: 16, 68 | splashRadius: 12, 69 | ) 70 | : null, 71 | ), 72 | ), 73 | ), 74 | ) 75 | .toList(), 76 | ); 77 | }), 78 | 79 | Obx(() { 80 | return Visibility( 81 | visible: controller.optionControllers.length < 6, 82 | child: TextButton( 83 | onPressed: controller.addOption, 84 | child: const Text("Add option"), 85 | ), 86 | ); 87 | }) 88 | ], 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /lib/src/auth/presentation/pages/phone_auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:intl_phone_number_input/intl_phone_number_input.dart'; 5 | import 'package:studyhive/shared/extensions/buttons.dart'; 6 | import 'package:studyhive/src/auth/presentation/manager/auth_controller.dart'; 7 | 8 | class PhoneAuthPage extends GetView { 9 | const PhoneAuthPage({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar(), 15 | body: SafeArea( 16 | child: Padding( 17 | padding: const EdgeInsets.all(18), 18 | child: Center( 19 | child: Column( 20 | children: [ 21 | Text( 22 | 'what_is_your_phone_number'.tr, 23 | style: Theme.of(context).textTheme.titleMedium!.copyWith( 24 | fontWeight: FontWeight.bold, 25 | fontSize: 20, 26 | ), 27 | textAlign: TextAlign.center, 28 | ), 29 | Padding( 30 | padding: const EdgeInsets.only(top: 6.0, bottom: 40), 31 | child: Text( 32 | 'send_verification_code'.tr, 33 | style: Theme.of(context).textTheme.bodyMedium, 34 | textAlign: TextAlign.center, 35 | ), 36 | ), 37 | Form( 38 | key: controller.formKey, 39 | child: Obx(() { 40 | return InternationalPhoneNumberInput( 41 | onInputValidated: (value) { 42 | controller.enableContinueButton = value; 43 | controller.update(); 44 | }, 45 | isEnabled: !controller.gettingOtp.value, 46 | onInputChanged: controller.onPhoneNumberChanged, 47 | keyboardType: TextInputType.phone, 48 | formatInput: true, 49 | autoFocus: true, 50 | countries: const ['GH', 'GB', 'NG'], 51 | selectorConfig: const SelectorConfig( 52 | selectorType: PhoneInputSelectorType.BOTTOM_SHEET, 53 | useEmoji: true, 54 | ), 55 | inputDecoration: InputDecoration( 56 | hintText: 'phone_number'.tr, 57 | contentPadding: const EdgeInsets.symmetric(horizontal: 20), 58 | ), 59 | searchBoxDecoration: InputDecoration( 60 | hintText: 'search_country'.tr, 61 | contentPadding: const EdgeInsets.symmetric(horizontal: 20), 62 | ), 63 | ); 64 | }), 65 | ), 66 | const Spacer(), 67 | SizedBox( 68 | width: double.infinity, 69 | child: GetBuilder( 70 | init: controller, 71 | builder: (controller) { 72 | return ElevatedButton.icon( 73 | onPressed: controller.enableContinueButton ? controller.submitPhoneNumber : null, 74 | label: Text('get_otp'.tr), 75 | icon: const Icon(IconlyBold.arrow_right), 76 | ).withLoading( 77 | loading: controller.gettingOtp, 78 | icon: const Icon(IconlyBold.arrow_right), 79 | text: 'sending_otp'.tr); 80 | }, 81 | ), 82 | ), 83 | ], 84 | ), 85 | ), 86 | ), 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/auth/data/repositories/auth_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:google_sign_in/google_sign_in.dart'; 5 | import 'package:studyhive/shared/error/failure.dart'; 6 | import 'package:studyhive/src/auth/domain/repositories/auth_repository.dart'; 7 | import 'package:studyhive/src/profile/data/local/data_sources/profile_local_database.dart'; 8 | import 'package:studyhive/src/profile/data/remote/data_sources/profile_remote_database.dart'; 9 | 10 | import '../../../../shared/network/network.dart'; 11 | import '../../../profile/domain/entities/profile.dart'; 12 | 13 | class AuthRepositoryImpl implements AuthRepository { 14 | final ProfileRemoteDatabase remoteDatabase; 15 | final ProfileLocalDatabase localDatabase; 16 | final NetworkInfo networkInfo; 17 | 18 | AuthRepositoryImpl({required this.remoteDatabase, required this.localDatabase, required this.networkInfo}); 19 | 20 | @override 21 | Future> continueWithApple(Profile profile) async { 22 | // TODO: implement continueWithApple 23 | throw UnimplementedError(); 24 | } 25 | 26 | @override 27 | Future> continueWithGoogle(Profile profile) async { 28 | // create an account with 29 | try { 30 | if (!await networkInfo.hasInternet()) { 31 | return Left(Failure('no_internet'.tr)); 32 | } 33 | 34 | GoogleSignInAccount? googleSignInAccount = await GoogleSignIn().signIn(); 35 | // Get the GoogleSignInAuthentication 36 | GoogleSignInAuthentication googleSignInAuthentication = await googleSignInAccount!.authentication; 37 | 38 | // Create the credential 39 | OAuthCredential credential = GoogleAuthProvider.credential( 40 | accessToken: googleSignInAuthentication.accessToken, 41 | idToken: googleSignInAuthentication.idToken, 42 | ); 43 | 44 | // Sign in with the credential 45 | UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(credential); 46 | final copiedProfile = profile.copyWith( 47 | id: userCredential.user!.uid, 48 | email: userCredential.user!.email!, 49 | name: userCredential.user!.displayName!, 50 | photoUrl: userCredential.user!.photoURL, 51 | ); 52 | final userExists = await remoteDatabase.exists(copiedProfile.id); 53 | if (userExists) { 54 | final appUser = await remoteDatabase.retrieve(copiedProfile.id); 55 | await localDatabase.save(appUser); 56 | } else { 57 | await remoteDatabase.save(copiedProfile); 58 | await localDatabase.save(copiedProfile); 59 | } 60 | return Right(userExists); 61 | } catch (error) { 62 | return const Left(Failure('Error signing in with Google')); 63 | } 64 | } 65 | 66 | @override 67 | Future> continueWithPhone(Profile profile) async { 68 | try { 69 | if (!await networkInfo.hasInternet()) { 70 | return Left(Failure('no_internet'.tr)); 71 | } 72 | 73 | final userExists = await remoteDatabase.exists(profile.id); 74 | if (userExists) { 75 | final appUser = await remoteDatabase.retrieve(profile.id); 76 | await localDatabase.save(appUser); 77 | } else { 78 | await remoteDatabase.save(profile); 79 | await localDatabase.save(profile); 80 | } 81 | return Right(userExists); 82 | } catch (error) { 83 | return Left(Failure(error.toString())); 84 | } 85 | } 86 | 87 | @override 88 | Future> signOut() async { 89 | try { 90 | if (!await networkInfo.hasInternet()) { 91 | return Left(Failure('no_internet'.tr)); 92 | } 93 | await FirebaseAuth.instance.signOut(); 94 | await localDatabase.delete(); 95 | return const Right(null); 96 | } catch (error) { 97 | return Left(Failure(error.toString())); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/create.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:iconly/iconly.dart'; 6 | import 'package:studyhive/shared/extensions/buttons.dart'; 7 | 8 | import '../manager/create_controller.dart'; 9 | 10 | class CreateHivePage extends GetView { 11 | const CreateHivePage({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final theme = Theme.of(context); 16 | return Scaffold( 17 | appBar: AppBar(), 18 | body: SafeArea( 19 | child: ListView( 20 | physics: const BouncingScrollPhysics(), 21 | padding: const EdgeInsets.all(14), 22 | children: [ 23 | Padding( 24 | padding: const EdgeInsets.only(bottom: 8.0), 25 | child: Text( 26 | "Create Your Hive", 27 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 28 | fontWeight: FontWeight.bold, 29 | fontSize: 20, 30 | ), 31 | textAlign: TextAlign.center, 32 | ), 33 | ), 34 | const Text( 35 | "A hive is a community of people who share a common interest.", 36 | textAlign: TextAlign.center, 37 | ), 38 | Padding( 39 | padding: const EdgeInsets.only(top: 20.0, bottom: 25), 40 | child: SizedBox( 41 | height: 120, 42 | child: InkWell( 43 | onTap: controller.chooseHiveProfile, 44 | child: Obx(() { 45 | if (controller.hiveProfile.value.isNotEmpty) { 46 | return CircleAvatar( 47 | radius: 45, 48 | backgroundImage: FileImage( 49 | File(controller.hiveProfile.value), 50 | ), 51 | ); 52 | } 53 | return CircleAvatar( 54 | radius: 40, 55 | backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), 56 | child: const Icon(IconlyBroken.image_2), 57 | ); 58 | }), 59 | ), 60 | ), 61 | ), 62 | Padding( 63 | padding: const EdgeInsets.only(bottom: 20.0), 64 | child: Obx(() { 65 | return TextFormField( 66 | controller: controller.nameController, 67 | autofocus: true, 68 | enabled: !controller.loading.value, 69 | decoration: const InputDecoration( 70 | contentPadding: EdgeInsets.symmetric(horizontal: 20), 71 | labelText: "Name", 72 | hintText: "Study Buddies", 73 | ), 74 | ); 75 | }), 76 | ), 77 | Padding( 78 | padding: const EdgeInsets.only(bottom: 5), 79 | child: Obx(() => ElevatedButton( 80 | onPressed: controller.enableButton.value ? () => controller.create() : null, 81 | child: const Text('Create Hive')) 82 | .withLoading(loading: controller.loading))), 83 | RichText( 84 | text: TextSpan( 85 | style: Theme.of(context).textTheme.bodySmall, 86 | text: "By creating a Hive, you agree to Study Hive's ", 87 | children: [ 88 | TextSpan( 89 | text: "community guidelines", 90 | style: TextStyle( 91 | color: Theme.of(context).colorScheme.primary, 92 | ), 93 | ), 94 | ], 95 | ), 96 | ), 97 | ], 98 | ), 99 | ), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/hive/data/repositories/hive_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:studyhive/shared/error/failure.dart'; 4 | import 'package:studyhive/shared/network/network.dart'; 5 | import 'package:studyhive/src/hive/data/local/data_sources/hive_local_database.dart'; 6 | import 'package:studyhive/src/hive/data/remote/data_sources/hive_remote_database.dart'; 7 | import 'package:studyhive/src/hive/domain/entities/hive.dart'; 8 | import 'package:studyhive/src/hive/domain/entities/message.dart'; 9 | import 'package:studyhive/src/hive/domain/repositories/hive_repository.dart'; 10 | 11 | class HiveRepositoryImpl implements HiveRepository { 12 | final HiveRemoteDatabase remoteDatabase; 13 | final HiveLocalDatabase localDatabase; 14 | final NetworkInfo networkInfo; 15 | 16 | const HiveRepositoryImpl({ 17 | required this.remoteDatabase, 18 | required this.localDatabase, 19 | required this.networkInfo, 20 | }); 21 | 22 | @override 23 | Future> create(Hive hive) async { 24 | try { 25 | final connected = await networkInfo.hasInternet(); 26 | if (!connected) { 27 | return Left(Failure('no_internet'.tr)); 28 | } 29 | final result = await remoteDatabase.create(hive); 30 | return Right(result); 31 | } catch (e) { 32 | return const Left(Failure("Error creating hive")); 33 | } 34 | } 35 | 36 | @override 37 | Future> delete(String hiveId) async { 38 | try { 39 | final connected = await networkInfo.hasInternet(); 40 | if (!connected) { 41 | return Left(Failure('no_internet'.tr)); 42 | } 43 | final result = await remoteDatabase.delete(hiveId); 44 | return Right(result); 45 | } catch (e) { 46 | return const Left(Failure("Error deleting hive")); 47 | } 48 | } 49 | 50 | @override 51 | Future> join({required String hiveId, required String userId}) async { 52 | try { 53 | final connected = await networkInfo.hasInternet(); 54 | if (!connected) { 55 | return Left(Failure('no_internet'.tr)); 56 | } 57 | final result = await remoteDatabase.join(hiveId: hiveId, userId: userId); 58 | return Right(result); 59 | } catch (e) { 60 | return const Left(Failure("Error joining hive")); 61 | } 62 | } 63 | 64 | @override 65 | Future>>> list(String userId) async { 66 | try { 67 | final connected = await networkInfo.hasInternet(); 68 | if (!connected) { 69 | final result = localDatabase.list(); 70 | return Right(result); 71 | } 72 | final result = remoteDatabase.list(userId); 73 | return Right(result); 74 | } catch (e) { 75 | return const Left(Failure("Error listing hives")); 76 | } 77 | } 78 | 79 | @override 80 | Future> update(Hive hive) async { 81 | try { 82 | final connected = await networkInfo.hasInternet(); 83 | if (!connected) { 84 | return Left(Failure('no_internet'.tr)); 85 | } 86 | final result = await remoteDatabase.update(hive); 87 | return Right(result); 88 | } catch (e) { 89 | return const Left(Failure("Error updating hive")); 90 | } 91 | } 92 | 93 | @override 94 | Future> leave({required String hiveId, required String userId}) async { 95 | // TODO: implement leave 96 | throw UnimplementedError(); 97 | } 98 | 99 | @override 100 | Future>> details(String hiveId) async { 101 | final connected = await networkInfo.hasInternet(); 102 | if (!connected) { 103 | return Left(Failure('no_internet'.tr)); 104 | } 105 | try { 106 | final result = remoteDatabase.details(hiveId); 107 | return Right(result); 108 | } catch (e) { 109 | return const Left(Failure("Error getting hive details")); 110 | } 111 | } 112 | 113 | @override 114 | Future> postMessage({required String hiveId, required Message message}) async { 115 | try { 116 | final connected = await networkInfo.hasInternet(); 117 | if (!connected) { 118 | return Left(Failure('no_internet'.tr)); 119 | } 120 | final result = await remoteDatabase.postMessage(hiveId: hiveId, message: message); 121 | return Right(result); 122 | } catch (e) { 123 | return const Left(Failure("Error posting message")); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/src/home/presentation/pages/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:ionicons/ionicons.dart'; 5 | import 'package:skeletons/skeletons.dart'; 6 | import 'package:studyhive/generated/assets.dart'; 7 | import 'package:studyhive/routes/app_pages.dart'; 8 | import 'package:studyhive/shared/extensions/strings.dart'; 9 | import 'package:studyhive/shared/ui/custom_listtile.dart'; 10 | import 'package:studyhive/shared/ui/empty_state.dart'; 11 | import 'package:studyhive/src/home/presentation/manager/home_controller.dart'; 12 | 13 | class HomePage extends GetView { 14 | const HomePage({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text( 21 | "STUDY HIVE", 22 | style: Theme.of(context).textTheme.titleLarge!.copyWith( 23 | fontWeight: FontWeight.bold, 24 | color: Theme.of(context).colorScheme.primary, 25 | fontSize: 18, 26 | ), 27 | ), 28 | actions: [ 29 | Padding( 30 | padding: const EdgeInsets.only(right: 5.0), 31 | child: IconButton( 32 | splashRadius: 22, 33 | onPressed: () { 34 | Get.toNamed(AppRoutes.settings); 35 | }, 36 | icon: const Icon(IconlyLight.setting), 37 | ), 38 | ) 39 | ], 40 | ), 41 | body: StreamBuilder( 42 | stream: controller.list(), 43 | builder: (BuildContext context, snapshot) { 44 | if (snapshot.hasError) { 45 | return const Center( 46 | child: Text('Something went wrong'), 47 | ); 48 | } 49 | if (snapshot.connectionState == ConnectionState.waiting) { 50 | return SkeletonItem( 51 | child: ListView.builder( 52 | padding: const EdgeInsets.symmetric(horizontal: 12), 53 | itemBuilder: (context, index) { 54 | return SkeletonListTile( 55 | titleStyle: const SkeletonLineStyle( 56 | height: 8, 57 | randomLength: true, 58 | borderRadius: BorderRadius.all(Radius.circular(8)), 59 | ), 60 | subtitleStyle: const SkeletonLineStyle( 61 | randomLength: true, 62 | height: 8, 63 | borderRadius: BorderRadius.all(Radius.circular(8)), 64 | ), 65 | leadingStyle: const SkeletonAvatarStyle( 66 | shape: BoxShape.circle, 67 | ), 68 | hasSubtitle: true, 69 | hasLeading: true, 70 | ); 71 | }, 72 | ), 73 | ); 74 | } 75 | if (snapshot.hasData && snapshot.data!.isEmpty) { 76 | return const EmptyState( 77 | text: "Start by creating your first Hive", 78 | asset: Assets.noHive, 79 | width: 300, 80 | ); 81 | } 82 | 83 | return RefreshIndicator( 84 | onRefresh: () async {}, 85 | child: ListView.builder( 86 | itemCount: snapshot.data?.length, 87 | itemBuilder: (BuildContext context, int index) { 88 | final hive = snapshot.data![index]; 89 | return CustomListTile( 90 | leading: hive.photoUrl == null ? Text(hive.name.initials) : null, 91 | url: hive.photoUrl, 92 | onTap: () { 93 | Get.toNamed('/hive/${hive.id}', arguments: {"hive": hive}); 94 | }, 95 | subtitle: "This is hive ${index + 1}", 96 | title: hive.name, 97 | trailing: Icon( 98 | IconlyLight.arrow_right_3, 99 | color: Get.theme.primaryColor, 100 | ), 101 | ); 102 | }, 103 | ), 104 | ); 105 | }, 106 | ), 107 | floatingActionButton: FloatingActionButton( 108 | tooltip: 'create', 109 | onPressed: () { 110 | Get.toNamed(AppRoutes.createHive); 111 | }, 112 | child: const Icon(Ionicons.add), 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/profile/presentation/pages/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:studyhive/shared/extensions/strings.dart'; 5 | import 'package:studyhive/shared/ui/custom_listtile.dart'; 6 | import 'package:studyhive/src/profile/presentation/manager/profile_controller.dart'; 7 | 8 | import '../../../../shared/ui/custom_bottomsheet.dart'; 9 | import '../../../settings/presentation/widgets/no_avatar.dart'; 10 | import '../../../settings/presentation/widgets/user_avatar.dart'; 11 | 12 | class ProfilePage extends GetView { 13 | const ProfilePage({Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Scaffold( 18 | appBar: AppBar(), 19 | body: SafeArea( 20 | child: SingleChildScrollView( 21 | padding: const EdgeInsets.only(top: 20), 22 | child: Column( 23 | children: [ 24 | SizedBox( 25 | height: 130, 26 | child: InkWell( 27 | onTap: () { 28 | showCustomBottomSheet( 29 | horizontalPadding: 0, 30 | height: controller.profile.photoUrl != null ? Get.height * 0.25 : Get.height * 0.18, 31 | child: Column( 32 | children: [ 33 | ListTile( 34 | onTap: () { 35 | Get.back(); 36 | }, 37 | leading: const Icon(IconlyLight.camera), 38 | title: Text("take_a_photo".tr), 39 | ), 40 | ListTile( 41 | onTap: () { 42 | Get.back(); 43 | }, 44 | leading: const Icon(IconlyLight.image), 45 | title: Text("choose_from_gallery".tr), 46 | ), 47 | if (controller.profile.photoUrl != null) 48 | ListTile( 49 | onTap: () { 50 | Get.back(); 51 | }, 52 | leading: const Icon(IconlyLight.delete), 53 | title: Text("delete_photo".tr), 54 | ), 55 | ], 56 | )); 57 | }, 58 | child: controller.profile.photoUrl != null 59 | ? Center( 60 | child: Hero( 61 | tag: controller.profile.id, 62 | child: UserAvatar( 63 | size: UserAvatarSize.lg, 64 | url: controller.profile.photoUrl!, 65 | ), 66 | ), 67 | ) 68 | : Center( 69 | child: Hero( 70 | tag: controller.profile.id, 71 | child: NoAvatar( 72 | size: NoAvatarSize.lg, 73 | initials: controller.profile.name.initials, 74 | ), 75 | ), 76 | )), 77 | ), 78 | const SizedBox(height: 20), 79 | CustomListTile( 80 | onTap: () {}, 81 | title: controller.profile.name, 82 | subtitle: "This name will be visible to your friends", 83 | leading: const Icon( 84 | IconlyLight.profile, 85 | ), 86 | trailing: const Icon( 87 | IconlyLight.edit, 88 | ), 89 | ), 90 | if (controller.profile.phoneNumber != null) 91 | CustomListTile( 92 | onTap: () {}, 93 | title: "Phone", 94 | subtitle: controller.profile.phoneNumber!, 95 | leading: const Icon( 96 | IconlyLight.call, 97 | ), 98 | ), 99 | if (controller.profile.email != null) 100 | CustomListTile( 101 | onTap: () {}, 102 | title: "Email", 103 | subtitle: controller.profile.email!, 104 | leading: const Icon( 105 | IconlyLight.message, 106 | ), 107 | ), 108 | ], 109 | ), 110 | ), 111 | ), 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/new_discussion.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:open_filex/open_filex.dart'; 5 | import 'package:studyhive/shared/theme/theme.dart'; 6 | import 'package:studyhive/src/hive/presentation/manager/discussions_controller.dart'; 7 | 8 | import '../../../../shared/utils/generate_file_icon.dart'; 9 | 10 | class NewDiscussionPage extends GetView { 11 | const NewDiscussionPage({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | leading: IconButton( 18 | splashRadius: 20, 19 | onPressed: () { 20 | Navigator.pop(context); 21 | }, 22 | icon: const Icon(Icons.close), 23 | ), 24 | actions: [ 25 | Padding( 26 | padding: const EdgeInsets.only(right: 15.0), 27 | child: Obx(() { 28 | return ElevatedButton(onPressed: controller.canPost.value ? () {} : null, child: const Text("Post")); 29 | }), 30 | ), 31 | ], 32 | ), 33 | body: SafeArea( 34 | child: ListView( 35 | children: [ 36 | Padding( 37 | padding: const EdgeInsets.only(bottom: 15.0, left: 16, right: 16), 38 | child: Row( 39 | children: [ 40 | const Padding( 41 | padding: EdgeInsets.only(right: 4.0), 42 | child: Icon( 43 | IconlyLight.user_1, 44 | size: 19, 45 | ), 46 | ), 47 | Text( 48 | "Private to My Hive", 49 | style: Theme.of(context).textTheme.bodySmall!.copyWith(fontWeight: FontWeight.bold), 50 | ), 51 | ], 52 | ), 53 | ), 54 | Padding( 55 | padding: const EdgeInsets.only(bottom: 14.0, right: 16, left: 16), 56 | child: TextFormField( 57 | controller: controller.textEditingController, 58 | keyboardType: TextInputType.multiline, 59 | maxLines: null, 60 | maxLength: 400, 61 | minLines: 1, 62 | decoration: const InputDecoration( 63 | contentPadding: inputPadding, 64 | hintText: "Share your thoughts...", 65 | ), 66 | ), 67 | ), 68 | Obx(() { 69 | return Column( 70 | children: List.generate( 71 | controller.attachments.length, 72 | (index) { 73 | return ListTile( 74 | onTap: () { 75 | // open file using open_file package 76 | OpenFilex.open(controller.attachments[index].path); 77 | }, 78 | leading: CircleAvatar( 79 | backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), 80 | radius: 20, 81 | child: generateIconFromExtension(controller.attachments[index].path.split('.').last)), 82 | title: Text( 83 | controller.attachments[index].path.split('/').last, 84 | style: Theme.of(context).textTheme.bodySmall!.copyWith( 85 | fontWeight: FontWeight.bold, 86 | ), 87 | maxLines: 2, 88 | overflow: TextOverflow.ellipsis, 89 | ), 90 | trailing: IconButton( 91 | iconSize: 20, 92 | onPressed: () { 93 | controller.attachments.removeAt(index); 94 | }, 95 | icon: const Icon(IconlyLight.delete), 96 | ), 97 | ); 98 | }, 99 | ), 100 | ); 101 | }), 102 | Padding( 103 | padding: const EdgeInsets.only(left: 5), 104 | child: Wrap( 105 | spacing: -5, 106 | children: [ 107 | IconButton( 108 | onPressed: controller.chooseImage, 109 | icon: const Icon(IconlyLight.image), 110 | iconSize: 20, 111 | splashRadius: 16, 112 | ), 113 | IconButton( 114 | onPressed: controller.chooseVideo, 115 | icon: const Icon(IconlyLight.video), 116 | iconSize: 20, 117 | ), 118 | IconButton( 119 | onPressed: controller.chooseDocument, 120 | icon: const Icon(IconlyLight.document), 121 | iconSize: 20, 122 | ), 123 | ], 124 | ), 125 | ) 126 | ], 127 | ), 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/new_question.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:studyhive/shared/ui/custom_bottomsheet.dart'; 5 | import 'package:studyhive/src/hive/domain/entities/message.dart'; 6 | import 'package:studyhive/src/hive/presentation/manager/questions_controller.dart'; 7 | 8 | import '../../../../shared/theme/theme.dart'; 9 | 10 | class NewQuestion extends GetView { 11 | const NewQuestion({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | leading: IconButton( 18 | splashRadius: 20, 19 | onPressed: () { 20 | Navigator.pop(context); 21 | }, 22 | icon: const Icon(Icons.close), 23 | ), 24 | actions: [ 25 | Padding( 26 | padding: const EdgeInsets.only(right: 15.0), 27 | child: ElevatedButton(onPressed: () {}, child: const Text("Ask")), 28 | ), 29 | ], 30 | ), 31 | body: ListView( 32 | padding: const EdgeInsets.all(14), 33 | children: [ 34 | Padding( 35 | padding: const EdgeInsets.only(bottom: 8.0), 36 | child: TextFormField( 37 | keyboardType: TextInputType.multiline, 38 | maxLines: null, 39 | maxLength: 400, 40 | minLines: 1, 41 | decoration: const InputDecoration( 42 | contentPadding: inputPadding, 43 | hintText: "Ask your question...", 44 | ), 45 | ), 46 | ), 47 | ListTile( 48 | onTap: () { 49 | showCustomBottomSheet( 50 | height: Get.height * 0.3, 51 | child: Column( 52 | children: List.generate( 53 | QuestionType.values.length, 54 | (index) => ListTile( 55 | trailing: Obx(() { 56 | return controller.questionType.value == QuestionType.values[index] 57 | ? Icon( 58 | IconlyLight.paper, 59 | color: Get.theme.colorScheme.primary, 60 | ) 61 | : const SizedBox.shrink(); 62 | }), 63 | onTap: () { 64 | controller.questionType.value = QuestionType.values[index]; 65 | Navigator.pop(context); 66 | }, 67 | title: Text( 68 | QuestionType.values[index].text, 69 | style: Theme.of(context).textTheme.bodyMedium!.copyWith( 70 | fontWeight: FontWeight.bold, 71 | ), 72 | ), 73 | ), 74 | ), 75 | )); 76 | }, 77 | title: Obx(() { 78 | return Text( 79 | controller.questionType.value.text, 80 | style: Theme.of(context).textTheme.bodySmall!.copyWith( 81 | fontWeight: FontWeight.bold, 82 | ), 83 | ); 84 | }), 85 | subtitle: Text( 86 | "Question Type", 87 | style: Theme.of(context).textTheme.bodySmall, 88 | ), 89 | trailing: const Icon( 90 | IconlyLight.arrow_down_2, 91 | size: 17, 92 | ), 93 | ), 94 | Padding( 95 | padding: const EdgeInsets.only(bottom: 10.0), 96 | child: ListTile( 97 | onTap: () {}, 98 | title: Text("Not Set", 99 | style: Theme.of(context).textTheme.bodySmall!.copyWith( 100 | fontWeight: FontWeight.bold, 101 | )), 102 | subtitle: Text( 103 | "Topic", 104 | style: Theme.of(context).textTheme.bodySmall, 105 | ), 106 | trailing: const Icon( 107 | IconlyLight.arrow_down_2, 108 | size: 17, 109 | ), 110 | ), 111 | ), 112 | Padding( 113 | padding: const EdgeInsets.only(left: 5), 114 | child: Wrap( 115 | spacing: -5, 116 | children: [ 117 | IconButton( 118 | onPressed: controller.chooseImage, 119 | icon: const Icon(IconlyLight.image), 120 | iconSize: 20, 121 | splashRadius: 16, 122 | ), 123 | IconButton( 124 | onPressed: controller.chooseVideo, 125 | icon: const Icon(IconlyLight.video), 126 | iconSize: 20, 127 | ), 128 | IconButton( 129 | onPressed: controller.chooseDocument, 130 | icon: const Icon(IconlyLight.document), 131 | iconSize: 20, 132 | ), 133 | ], 134 | ), 135 | ) 136 | ], 137 | ), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/src/auth/presentation/manager/auth_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:intl_phone_number_input/intl_phone_number_input.dart'; 5 | import 'package:pinput/pinput.dart'; 6 | 7 | import '../../../../routes/app_pages.dart'; 8 | import '../../../../shared/network/network.dart'; 9 | import '../../../../shared/ui/snackbars.dart'; 10 | import '../../../profile/domain/entities/profile.dart'; 11 | import '../../domain/repositories/auth_repository.dart'; 12 | import '../pages/verify_otp.dart'; 13 | 14 | class AuthController extends GetxController { 15 | bool enableContinueButton = false; 16 | bool enableVerifyButton = false; 17 | String? phoneNumber; 18 | RxBool isVerifying = false.obs; 19 | RxBool gettingOtp = false.obs; 20 | final formKey = GlobalKey(); 21 | final AuthRepository _authRepository; 22 | 23 | AuthController(this._authRepository); 24 | 25 | final otpController = TextEditingController(); 26 | 27 | final networkInfo = Get.find(); 28 | 29 | void onPhoneNumberChanged(PhoneNumber? value) { 30 | phoneNumber = value!.phoneNumber; 31 | } 32 | 33 | void onOtpChanged(String value) { 34 | enableVerifyButton = value.length == 6; 35 | update(); 36 | } 37 | 38 | Future submitPhoneNumber() async { 39 | if (!await networkInfo.hasInternet()) { 40 | showErrorSnackbar(message: 'no_internet'.tr); 41 | return; 42 | } 43 | 44 | if (formKey.currentState!.validate()) { 45 | formKey.currentState!.save(); 46 | gettingOtp.value = true; 47 | await FirebaseAuth.instance.verifyPhoneNumber( 48 | phoneNumber: phoneNumber!, 49 | verificationCompleted: (PhoneAuthCredential credential) { 50 | otpController.setText(credential.smsCode!); 51 | }, 52 | verificationFailed: (FirebaseAuthException e) { 53 | gettingOtp.value = false; 54 | if (e.code == 'invalid-phone-number') { 55 | showErrorSnackbar(message: 'invalid_phone_number'.tr); 56 | } 57 | if (e.code == 'too-many-requests') { 58 | showErrorSnackbar(message: 'too_many_requests'.tr); 59 | } 60 | if (e.code == 'invalid-verification-code') { 61 | showErrorSnackbar(message: 'invalid_verification_code'.tr); 62 | } 63 | }, 64 | codeSent: (String verificationId, int? resendToken) { 65 | gettingOtp.value = false; 66 | Get.to(() => VerifyOtp( 67 | verificationId: verificationId, 68 | resendToken: resendToken, 69 | )); 70 | }, 71 | codeAutoRetrievalTimeout: (String verificationId) { 72 | gettingOtp.value = false; 73 | // showErrorSnackbar(message: 'code_auto_retrieval_timeout'.tr); 74 | }, 75 | ); 76 | } 77 | } 78 | 79 | Future verifyOtp({required String verificationId}) async { 80 | try { 81 | isVerifying.value = true; 82 | PhoneAuthCredential credential = 83 | PhoneAuthProvider.credential(verificationId: verificationId, smsCode: otpController.text); 84 | 85 | final user = await FirebaseAuth.instance.signInWithCredential(credential); 86 | final results = await _authRepository.continueWithPhone(Profile.empty().copyWith( 87 | phoneNumber: user.user!.phoneNumber, 88 | id: user.user!.uid, 89 | )); 90 | results.fold((failure) { 91 | isVerifying.value = false; 92 | showErrorSnackbar(message: failure.message); 93 | }, (exists) { 94 | isVerifying.value = false; 95 | if (exists) { 96 | Get.offAllNamed(AppRoutes.home); 97 | } else { 98 | Get.offAllNamed(AppRoutes.setupProfile); 99 | } 100 | }); 101 | } on FirebaseAuthException catch (e) { 102 | isVerifying.value = false; 103 | if (e.code == 'invalid-verification-code') { 104 | showErrorSnackbar(message: 'invalid_verification_code'.tr); 105 | } 106 | if (e.code == 'too-many-requests') { 107 | showErrorSnackbar(message: 'too_many_requests'.tr); 108 | } 109 | } 110 | } 111 | 112 | Future resendOtp({required String phoneNumber, int? resendToken}) async { 113 | await FirebaseAuth.instance.verifyPhoneNumber( 114 | phoneNumber: phoneNumber, 115 | verificationCompleted: (PhoneAuthCredential credential) { 116 | otpController.setText(credential.smsCode!); 117 | }, 118 | verificationFailed: (FirebaseAuthException e) { 119 | gettingOtp.value = false; 120 | if (e.code == 'invalid-phone-number') { 121 | showErrorSnackbar(message: 'invalid_phone_number'.tr); 122 | } 123 | if (e.code == 'too-many-requests') { 124 | showErrorSnackbar(message: 'too_many_requests'.tr); 125 | } 126 | if (e.code == 'invalid-verification-code') { 127 | showErrorSnackbar(message: 'invalid_verification_code'.tr); 128 | } 129 | }, 130 | codeSent: (String verificationId, int? resendToken) { 131 | showSuccessSnackbar(message: 'code_sent'.trParams({'phoneNumber': phoneNumber})); 132 | }, 133 | codeAutoRetrievalTimeout: (String verificationId) { 134 | // showErrorSnackbar(message: 'code_auto_retrieval_timeout'.tr); 135 | }, 136 | forceResendingToken: resendToken, 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/hive/domain/entities/media.freezed.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | // GENERATED CODE - DO NOT MODIFY BY HAND 3 | // ignore_for_file: type=lint 4 | // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark 5 | 6 | part of 'media.dart'; 7 | 8 | // ************************************************************************** 9 | // FreezedGenerator 10 | // ************************************************************************** 11 | 12 | T _$identity(T value) => value; 13 | 14 | final _privateConstructorUsedError = UnsupportedError( 15 | 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 16 | 17 | Media _$MediaFromJson(Map json) { 18 | return _Media.fromJson(json); 19 | } 20 | 21 | /// @nodoc 22 | mixin _$Media { 23 | String get url => throw _privateConstructorUsedError; 24 | FileTypeOption get type => throw _privateConstructorUsedError; 25 | 26 | Map toJson() => throw _privateConstructorUsedError; 27 | @JsonKey(ignore: true) 28 | $MediaCopyWith get copyWith => throw _privateConstructorUsedError; 29 | } 30 | 31 | /// @nodoc 32 | abstract class $MediaCopyWith<$Res> { 33 | factory $MediaCopyWith(Media value, $Res Function(Media) then) = 34 | _$MediaCopyWithImpl<$Res, Media>; 35 | @useResult 36 | $Res call({String url, FileTypeOption type}); 37 | } 38 | 39 | /// @nodoc 40 | class _$MediaCopyWithImpl<$Res, $Val extends Media> 41 | implements $MediaCopyWith<$Res> { 42 | _$MediaCopyWithImpl(this._value, this._then); 43 | 44 | // ignore: unused_field 45 | final $Val _value; 46 | // ignore: unused_field 47 | final $Res Function($Val) _then; 48 | 49 | @pragma('vm:prefer-inline') 50 | @override 51 | $Res call({ 52 | Object? url = null, 53 | Object? type = null, 54 | }) { 55 | return _then(_value.copyWith( 56 | url: null == url 57 | ? _value.url 58 | : url // ignore: cast_nullable_to_non_nullable 59 | as String, 60 | type: null == type 61 | ? _value.type 62 | : type // ignore: cast_nullable_to_non_nullable 63 | as FileTypeOption, 64 | ) as $Val); 65 | } 66 | } 67 | 68 | /// @nodoc 69 | abstract class _$$_MediaCopyWith<$Res> implements $MediaCopyWith<$Res> { 70 | factory _$$_MediaCopyWith(_$_Media value, $Res Function(_$_Media) then) = 71 | __$$_MediaCopyWithImpl<$Res>; 72 | @override 73 | @useResult 74 | $Res call({String url, FileTypeOption type}); 75 | } 76 | 77 | /// @nodoc 78 | class __$$_MediaCopyWithImpl<$Res> extends _$MediaCopyWithImpl<$Res, _$_Media> 79 | implements _$$_MediaCopyWith<$Res> { 80 | __$$_MediaCopyWithImpl(_$_Media _value, $Res Function(_$_Media) _then) 81 | : super(_value, _then); 82 | 83 | @pragma('vm:prefer-inline') 84 | @override 85 | $Res call({ 86 | Object? url = null, 87 | Object? type = null, 88 | }) { 89 | return _then(_$_Media( 90 | url: null == url 91 | ? _value.url 92 | : url // ignore: cast_nullable_to_non_nullable 93 | as String, 94 | type: null == type 95 | ? _value.type 96 | : type // ignore: cast_nullable_to_non_nullable 97 | as FileTypeOption, 98 | )); 99 | } 100 | } 101 | 102 | /// @nodoc 103 | @JsonSerializable() 104 | class _$_Media implements _Media { 105 | const _$_Media({required this.url, required this.type}); 106 | 107 | factory _$_Media.fromJson(Map json) => 108 | _$$_MediaFromJson(json); 109 | 110 | @override 111 | final String url; 112 | @override 113 | final FileTypeOption type; 114 | 115 | @override 116 | String toString() { 117 | return 'Media(url: $url, type: $type)'; 118 | } 119 | 120 | @override 121 | bool operator ==(dynamic other) { 122 | return identical(this, other) || 123 | (other.runtimeType == runtimeType && 124 | other is _$_Media && 125 | (identical(other.url, url) || other.url == url) && 126 | (identical(other.type, type) || other.type == type)); 127 | } 128 | 129 | @JsonKey(ignore: true) 130 | @override 131 | int get hashCode => Object.hash(runtimeType, url, type); 132 | 133 | @JsonKey(ignore: true) 134 | @override 135 | @pragma('vm:prefer-inline') 136 | _$$_MediaCopyWith<_$_Media> get copyWith => 137 | __$$_MediaCopyWithImpl<_$_Media>(this, _$identity); 138 | 139 | @override 140 | Map toJson() { 141 | return _$$_MediaToJson( 142 | this, 143 | ); 144 | } 145 | } 146 | 147 | abstract class _Media implements Media { 148 | const factory _Media( 149 | {required final String url, 150 | required final FileTypeOption type}) = _$_Media; 151 | 152 | factory _Media.fromJson(Map json) = _$_Media.fromJson; 153 | 154 | @override 155 | String get url; 156 | @override 157 | FileTypeOption get type; 158 | @override 159 | @JsonKey(ignore: true) 160 | _$$_MediaCopyWith<_$_Media> get copyWith => 161 | throw _privateConstructorUsedError; 162 | } 163 | -------------------------------------------------------------------------------- /lib/src/hive/presentation/pages/hive.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:iconly/iconly.dart'; 4 | import 'package:studyhive/shared/theme/theme.dart'; 5 | import 'package:studyhive/shared/ui/custom_bottomsheet.dart'; 6 | import 'package:studyhive/shared/ui/empty_state.dart'; 7 | import 'package:studyhive/shared/ui/spinner.dart'; 8 | import 'package:studyhive/src/hive/presentation/manager/hive_controller.dart'; 9 | 10 | import '../../../../generated/assets.dart'; 11 | import '../../../../shared/ui/custom_listtile.dart'; 12 | import 'new_material.dart'; 13 | import 'new_poll.dart'; 14 | import 'new_question.dart'; 15 | 16 | class HivePage extends GetView { 17 | const HivePage({Key? key}) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text(controller.hive.name), 24 | leading: IconButton( 25 | icon: const BackButtonIcon(), 26 | onPressed: () { 27 | Get.back(); 28 | }, 29 | splashRadius: 20, 30 | ), 31 | actions: [ 32 | Padding( 33 | padding: const EdgeInsets.only(right: 5.0), 34 | child: IconButton( 35 | splashRadius: 22, 36 | onPressed: () {}, 37 | icon: const Icon(IconlyLight.setting), 38 | ), 39 | ) 40 | ], 41 | ), 42 | body: SafeArea( 43 | child: Column( 44 | children: [ 45 | Expanded( 46 | child: StreamBuilder( 47 | stream: controller.details(), 48 | builder: (context, snapshot) { 49 | if (snapshot.connectionState == ConnectionState.waiting) { 50 | return const Center(child: Spinner()); 51 | } else if (snapshot.hasError) { 52 | return const Text("We had an error"); 53 | } 54 | final data = snapshot.data!; 55 | return data.conversations.isEmpty 56 | ? const EmptyState(text: "", asset: Assets.discussion) 57 | : ListView.builder( 58 | padding: const EdgeInsets.symmetric(horizontal: 10.0), 59 | itemCount: data.conversations.length, 60 | itemBuilder: (context, index) { 61 | final message = data.conversations[index]; 62 | return Text(message.text ?? ""); 63 | }, 64 | ); 65 | }, 66 | ), 67 | ), 68 | Padding( 69 | padding: const EdgeInsets.symmetric(horizontal: 10.0), 70 | child: TextFormField( 71 | controller: controller.textController, 72 | maxLines: 5, 73 | minLines: 1, 74 | decoration: InputDecoration( 75 | contentPadding: inputPadding, 76 | hintText: 'Share your thoughts...', 77 | suffixIcon: Obx(() { 78 | return IconButton( 79 | onPressed: controller.showSendButton.value 80 | ? () { 81 | controller.sendMessage(); 82 | } 83 | : null, 84 | icon: const Icon(IconlyLight.send), 85 | ); 86 | }), 87 | prefixIcon: IconButton( 88 | onPressed: () { 89 | showCustomBottomSheet( 90 | horizontalPadding: 0, 91 | height: Get.height * 0.24, 92 | child: Column( 93 | children: [ 94 | CustomListTile( 95 | onTap: () { 96 | Get.back(); 97 | Get.to(() => const NewQuestion(), fullscreenDialog: true); 98 | }, 99 | title: "Ask a Question", 100 | leading: const Icon(IconlyLight.paper), 101 | ), 102 | CustomListTile( 103 | onTap: () { 104 | Get.back(); 105 | Get.to(() => const NewMaterial(), fullscreenDialog: true); 106 | }, 107 | title: "Share a Material", 108 | leading: const Icon(IconlyLight.paper_plus), 109 | ), 110 | CustomListTile( 111 | onTap: () { 112 | Get.back(); 113 | Get.to(() => const NewPoll(), fullscreenDialog: true); 114 | }, 115 | title: "Create a Poll", 116 | leading: const Icon(IconlyLight.chart), 117 | ), 118 | ], 119 | ), 120 | ); 121 | }, 122 | icon: const Icon(IconlyLight.activity), 123 | ), 124 | ), 125 | // onFieldSubmitted: (value) {}, 126 | ), 127 | ) 128 | ], 129 | ), 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/translations/translation.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class Localization extends Translations { 4 | @override 5 | // TODO: implement keys 6 | Map> get keys => { 7 | 'en_US': { 8 | 'welcome': 'Welcome to Study Hive!', 9 | 'welcome_sub_text': 'Join the over 1000 students using Study Hive to study together and ace their exams!', 10 | 'continue_with_google': 'Continue with Google', 11 | 'signing_in': 'Signing in...', 12 | 'continue_with_phone': 'Continue with Phone', 13 | 'what_is_your_phone_number': 'What is your phone number?', 14 | 'phone_number': 'Phone Number', 15 | 'send_verification_code': 'We will send you a verification code', 16 | 'get_otp': 'Get OTP', 17 | 'sending_otp': 'Sending OTP...', 18 | 'enter_verification_code': 'Enter the verification code sent to', 19 | 'we_sent_you_an_sms_code': 'We sent you an SMS code', 20 | 'on_this_number': 'On this number: ', 21 | 'didnt_get_code': 'Didn\'t get the code? ', 22 | 'resend_code': 'Resend Code', 23 | "verify_and_proceed": "Verify and Proceed", 24 | "verifying": "Verifying...", 25 | "invalid_phone_number": "The provided phone number is not valid.", 26 | "code_sent": "Code sent to @phoneNumber", 27 | "search_country": "Search by country name or dial code", 28 | 'change_number': 'Not your number? Change it.', 29 | 'notifications': 'Notifications', 30 | 'settings': 'Settings', 31 | 'create_your_hive': 'Create Your Hive', 32 | 'your_account': 'Your Account', 33 | 'your_personal_account': 'Your personal account', 34 | 'what_is_your_name': 'What is your name?', 35 | 'full_name': 'Full Name', 36 | 'we_will_use_this_for_your_profile': 'We will use this for your profile', 37 | 'continue': 'Continue', 38 | 'take_a_photo': 'Take a photo', 39 | 'choose_from_gallery': 'Choose from gallery', 40 | 'delete_photo': 'Delete photo', 41 | 'logout': 'Log out', 42 | 'logout_from_studyhive': 'Log out from Study Hive', 43 | 'no_internet': 'Please check your internet connection', 44 | "theme": "Theme", 45 | "light_mode": "Light Mode", 46 | "dark_mode": "Dark Mode", 47 | "privacy_security": "Privacy & Security", 48 | 'control_your_privacy': 'Control your privacy and security', 49 | "invite_a_friend": "Invite a friend", 50 | "share_studyhive_with_your_friends": "Share Study Hive with your friends", 51 | "logout_prompt": "Are you sure you want to log out?", 52 | 'cancel': 'Cancel', 53 | 'too_many_requests': 'Too many requests. Please try again later.', 54 | 'invalid_verification_code': 'Invalid verification code.', 55 | }, 56 | 'fr_FR': { 57 | 'welcome': 'Bienvenue sur Study Hive !', 58 | 'welcome_sub_text': 59 | 'Rejoignez plus de 1000 étudiants utilisant Study Hive pour étudier ensemble et réussir leurs examens !', 60 | 'continue_with_google': 'Continuer avec Google', 61 | 'signing_in': 'Connexion en cours...', 62 | 'continue_with_phone': 'Continuer avec le téléphone', 63 | 'what_is_your_phone_number': 'Quel est votre numéro de téléphone ?', 64 | 'phone_number': 'Numéro de téléphone', 65 | 'send_verification_code': 'Nous vous enverrons un code de vérification', 66 | 'get_otp': 'Obtenir OTP', 67 | 'sending_otp': 'Envoi de OTP en cours...', 68 | 'enter_verification_code': 'Entrez le code de vérification envoyé à', 69 | 'we_sent_you_an_sms_code': 'Nous vous avons envoyé un code SMS', 70 | 'on_this_number': 'Sur ce numéro : ', 71 | 'didnt_get_code': "Vous n'avez pas reçu le code ? ", 72 | 'resend_code': 'Renvoyer le code', 73 | "verify_and_proceed": "Vérifier et continuer", 74 | "verifying": "Vérification en cours...", 75 | "invalid_phone_number": "Le numéro de téléphone fourni n'est pas valide.", 76 | "code_sent": "Code envoyé à @phoneNumber", 77 | "search_country": "Rechercher par nom de pays ou code d'appel", 78 | 'change_number': "Ce n'est pas votre numéro ? Le changer.", 79 | 'notifications': 'Notifications', 80 | 'settings': 'Paramètres', 81 | 'create_your_hive': 'Créer votre ruche', 82 | 'your_account': 'Votre compte', 83 | 'your_personal_account': 'Votre compte personnel', 84 | 'what_is_your_name': 'Comment vous appelez-vous ?', 85 | 'full_name': 'Nom complet', 86 | 'we_will_use_this_for_your_profile': 'Nous utiliserons cela pour votre profil', 87 | 'continue': 'Continuer', 88 | 'take_a_photo': 'Prendre une photo', 89 | 'choose_from_gallery': 'Choisir depuis la galerie', 90 | 'delete_photo': 'Supprimer la photo', 91 | 'logout': 'Se déconnecter', 92 | 'logout_from_studyhive': 'Se déconnecter de Study Hive', 93 | 'no_internet': 'Veuillez vérifier votre connexion internet', 94 | "theme": "Thème", 95 | "light_mode": "Mode clair", 96 | "dark_mode": "Mode sombre", 97 | "privacy_security": "Confidentialité et sécurité", 98 | 'control_your_privacy': 'Contrôlez votre vie privée et votre sécurité', 99 | "invite_a_friend": "Inviter un ami", 100 | "share_studyhive_with_your_friends": "Partagez Study Hive avec vos amis", 101 | "logout_prompt": "Êtes-vous sûr de vouloir vous déconnecter ?", 102 | 'cancel': 'Annuler', 103 | 'too_many_requests': 'Trop de requêtes', 104 | 'invalid_verification_code': 'Code de vérification invalide.', 105 | }, 106 | }; 107 | } 108 | --------------------------------------------------------------------------------