├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── 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 │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner.xcodeproj │ ├── project.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── Runner.xcscheme ├── assets ├── top.png ├── intro_five ├── main.png ├── splash.png ├── trophy.png ├── intro_five.png ├── intro_four.png ├── intro_one.png ├── intro_six.png ├── intro_three.png ├── intro_two.png ├── fonts │ ├── SF_Atarian_System.ttf │ └── SF_Atarian_System_Bold.ttf └── default.json ├── screenshots ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg └── snap.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── ic_launcher-web.png │ │ │ ├── res │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── splash.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── em2 │ │ │ │ │ └── snaphunt │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── lib ├── constants │ ├── game_status_enum.dart │ └── app_theme.dart ├── widgets │ ├── common │ │ ├── custom_scroll.dart │ │ ├── wave.dart │ │ ├── card_textfield.dart │ │ ├── fancy_alert_dialog.dart │ │ ├── loading.dart │ │ ├── countdown.dart │ │ ├── fancy_button.dart │ │ ├── camera.dart │ │ └── hunt_game.dart │ └── multiplayer │ │ ├── room_loading.dart │ │ ├── player_await_button.dart │ │ ├── host_start_button.dart │ │ ├── create_buttons.dart │ │ ├── lobby_buttons.dart │ │ ├── room_exit_dialog.dart │ │ └── join_room_dialog.dart ├── model │ ├── hunt.dart │ ├── player.dart │ ├── user.dart │ └── game.dart ├── ui │ ├── singleplayer │ │ ├── singleplayer.dart │ │ ├── single_result.dart │ │ └── single_settings.dart │ ├── multiplayer │ │ ├── multiplayer.dart │ │ ├── create_room.dart │ │ ├── lobby.dart │ │ ├── multiplayer_result.dart │ │ └── room.dart │ ├── how_to_play.dart │ ├── login.dart │ └── home.dart ├── services │ ├── connectivity.dart │ └── auth.dart ├── stores │ ├── player_hunt_model.dart │ ├── game_model.dart │ └── hunt_model.dart ├── utils │ └── utils.dart ├── main.dart ├── routes.dart └── data │ └── repository.dart ├── .metadata ├── test └── widget_test.dart ├── LICENSE ├── .gitignore ├── pubspec.yaml ├── README.md └── pubspec.lock /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /assets/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/top.png -------------------------------------------------------------------------------- /assets/intro_five: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_five -------------------------------------------------------------------------------- /assets/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/main.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/trophy.png -------------------------------------------------------------------------------- /screenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/screenshots/1.jpg -------------------------------------------------------------------------------- /screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/screenshots/2.jpg -------------------------------------------------------------------------------- /screenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/screenshots/3.jpg -------------------------------------------------------------------------------- /screenshots/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/screenshots/4.jpg -------------------------------------------------------------------------------- /screenshots/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/screenshots/5.jpg -------------------------------------------------------------------------------- /assets/intro_five.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_five.png -------------------------------------------------------------------------------- /assets/intro_four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_four.png -------------------------------------------------------------------------------- /assets/intro_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_one.png -------------------------------------------------------------------------------- /assets/intro_six.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_six.png -------------------------------------------------------------------------------- /assets/intro_three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_three.png -------------------------------------------------------------------------------- /assets/intro_two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/intro_two.png -------------------------------------------------------------------------------- /screenshots/snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/screenshots/snap.png -------------------------------------------------------------------------------- /assets/fonts/SF_Atarian_System.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/fonts/SF_Atarian_System.ttf -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | 5 | -------------------------------------------------------------------------------- /assets/fonts/SF_Atarian_System_Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/assets/fonts/SF_Atarian_System_Bold.ttf -------------------------------------------------------------------------------- /lib/constants/game_status_enum.dart: -------------------------------------------------------------------------------- 1 | enum GameStatus { 2 | waiting, 3 | game, 4 | cancelled, 5 | kicked, 6 | full, 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffffff 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/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/snap-hunt/snaphunt/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snap-hunt/snaphunt/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/snap-hunt/snaphunt/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "words": [ 4 | "cars", 5 | "sky", 6 | "chair", 7 | "table", 8 | "room", 9 | "bag", 10 | "food", 11 | "cat", 12 | "mobile phone", 13 | "tree" 14 | ] 15 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/constants/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const fancy_button_style = 4 | TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold); 5 | 6 | final List user_colors = [ 7 | Colors.orange, 8 | Colors.purple, 9 | Colors.yellow, 10 | Colors.red, 11 | Colors.green, 12 | ]; 13 | -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: cc949a8e8b9cf394b9290a8e80f87af3e207dce5 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/widgets/common/custom_scroll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Removes list overscroll animation 4 | // https://stackoverflow.com/questions/51119795/how-to-remove-scroll-glow 5 | class NoOverFlowScrollBehavior extends ScrollBehavior { 6 | @override 7 | Widget buildViewportChrome( 8 | BuildContext context, Widget child, AxisDirection axisDirection) { 9 | return child; 10 | } 11 | } -------------------------------------------------------------------------------- /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/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /lib/model/hunt.dart: -------------------------------------------------------------------------------- 1 | class Hunt { 2 | String word; 3 | bool isFound; 4 | 5 | Hunt({ 6 | this.word, 7 | this.isFound = false, 8 | }); 9 | 10 | Hunt.fromJson(Map json) { 11 | word = json['word']; 12 | isFound = json['isFound']; 13 | } 14 | 15 | Map toJson() { 16 | final Map data = new Map(); 17 | data['word'] = this.word; 18 | data['isFound'] = this.isFound; 19 | return data; 20 | } 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 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/widgets/multiplayer/room_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RoomLoading extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Center( 7 | child: Column( 8 | mainAxisSize: MainAxisSize.min, 9 | mainAxisAlignment: MainAxisAlignment.center, 10 | crossAxisAlignment: CrossAxisAlignment.center, 11 | children: [ 12 | CircularProgressIndicator(), 13 | SizedBox(height: 10), 14 | Text('Retrieving game') 15 | ], 16 | ), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/model/player.dart: -------------------------------------------------------------------------------- 1 | import 'package:snaphunt/model/user.dart'; 2 | 3 | class Player { 4 | User user; 5 | int score; 6 | String status; 7 | 8 | Player({this.user, this.score = 0, this.status = 'active'}); 9 | 10 | Player.fromJson(Map json, Map userJson) { 11 | user = User.fromJson(userJson); 12 | score = json['score']; 13 | status = json['status']; 14 | } 15 | 16 | Map toJson() { 17 | final Map data = new Map(); 18 | data['user'] = this.user.toJson(); 19 | data['score'] = this.score; 20 | data['status'] = this.status; 21 | return data; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/widgets/multiplayer/player_await_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PlayerAwaitButton extends StatelessWidget { 4 | final bool canStartGame; 5 | 6 | const PlayerAwaitButton({ 7 | Key key, 8 | this.canStartGame, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | child: Column( 15 | mainAxisSize: MainAxisSize.min, 16 | children: [ 17 | CircularProgressIndicator(), 18 | const SizedBox(height: 14), 19 | Text(canStartGame ? 'Waiting for host' : 'Waiting for player') 20 | ], 21 | ), 22 | ); 23 | } 24 | } -------------------------------------------------------------------------------- /lib/model/user.dart: -------------------------------------------------------------------------------- 1 | class User { 2 | String uid; 3 | String email; 4 | String photoUrl; 5 | String displayName; 6 | 7 | User({ 8 | this.uid, 9 | this.email, 10 | this.photoUrl, 11 | this.displayName, 12 | }); 13 | 14 | User.fromJson(Map json) { 15 | uid = json['uid']; 16 | email = json['email']; 17 | photoUrl = json['photoURL']; 18 | displayName = json['displayName']; 19 | } 20 | 21 | Map toJson() { 22 | final Map data = new Map(); 23 | data['uid'] = this.uid; 24 | data['email'] = this.email; 25 | data['photoURL'] = this.photoUrl; 26 | data['displayName'] = this.displayName; 27 | return data; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/ui/singleplayer/singleplayer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:snaphunt/stores/hunt_model.dart'; 4 | import 'package:snaphunt/utils/utils.dart'; 5 | import 'package:snaphunt/widgets/common/hunt_game.dart'; 6 | 7 | class SinglePlayer extends StatelessWidget { 8 | final int numOfObjects; 9 | final int duration; 10 | 11 | const SinglePlayer({Key key, this.numOfObjects, this.duration}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return ChangeNotifierProvider( 17 | builder: (_) => new HuntModel( 18 | objects: generateHuntObjects(numOfObjects), 19 | timeLimit: DateTime.now().add(Duration(minutes: duration)), 20 | ), 21 | child: HuntGame( 22 | title: 'SinglePlayer!', 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/widgets/common/wave.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:wave/config.dart'; 3 | import 'package:wave/wave.dart'; 4 | 5 | class CustomWaveWidget extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return WaveWidget( 9 | duration: 1, 10 | config: CustomConfig( 11 | gradients: [ 12 | [Colors.orange, Color(0xEEF44336)], 13 | [Colors.red[200], Color(0x77E57373)], 14 | [Colors.orange, Color(0x66FF9800)], 15 | [Colors.yellow[800], Color(0x55FFEB3B)] 16 | ], 17 | durations: [35000, 19440, 10800, 6000], 18 | heightPercentages: [0.20, 0.23, 0.25, 0.30], 19 | blur: MaskFilter.blur(BlurStyle.solid, 2), 20 | gradientBegin: Alignment.bottomLeft, 21 | gradientEnd: Alignment.topRight, 22 | ), 23 | waveAmplitude: 1.0, 24 | backgroundColor: Colors.white, 25 | size: Size(double.maxFinite, 50.0)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/services/connectivity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity/connectivity.dart'; 4 | 5 | enum ConnectivityStatus { WiFi, Cellular, Offline } 6 | 7 | class ConnectivityService { 8 | StreamController connectionStatusController = 9 | StreamController(); 10 | 11 | ConnectivityService() { 12 | Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { 13 | connectionStatusController.add(_getStatusFromResult(result)); 14 | }); 15 | } 16 | 17 | void dispose() { 18 | connectionStatusController.close(); 19 | } 20 | 21 | ConnectivityStatus _getStatusFromResult(ConnectivityResult result) { 22 | switch (result) { 23 | case ConnectivityResult.mobile: 24 | return ConnectivityStatus.Cellular; 25 | case ConnectivityResult.wifi: 26 | return ConnectivityStatus.WiFi; 27 | case ConnectivityResult.none: 28 | return ConnectivityStatus.Offline; 29 | default: 30 | return ConnectivityStatus.Offline; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | void main() { 12 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 13 | // Build our app and trigger a frame. 14 | // await tester.pumpWidget(App()); 15 | 16 | // Verify that our counter starts at 0. 17 | expect(find.text('0'), findsOneWidget); 18 | expect(find.text('1'), findsNothing); 19 | 20 | // Tap the '+' icon and trigger a frame. 21 | await tester.tap(find.byIcon(Icons.add)); 22 | await tester.pump(); 23 | 24 | // Verify that our counter has incremented. 25 | expect(find.text('0'), findsNothing); 26 | expect(find.text('1'), findsOneWidget); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SnapHunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/widgets/multiplayer/host_start_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/constants/app_theme.dart'; 3 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 4 | 5 | class HostStartButton extends StatelessWidget { 6 | final bool canStartGame; 7 | final Function onGameStart; 8 | 9 | const HostStartButton({ 10 | Key key, 11 | this.canStartGame, 12 | this.onGameStart, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | child: Column( 19 | mainAxisSize: MainAxisSize.max, 20 | crossAxisAlignment: CrossAxisAlignment.center, 21 | children: [ 22 | FancyButton( 23 | child: Text( 24 | 'BEGIN HUNT', 25 | style: fancy_button_style, 26 | ), 27 | color: canStartGame ? Colors.deepOrangeAccent : Colors.grey, 28 | size: 70, 29 | onPressed: canStartGame ? onGameStart : null, 30 | ), 31 | SizedBox(height: 10), 32 | ], 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/ui/multiplayer/multiplayer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:snaphunt/model/game.dart'; 4 | import 'package:snaphunt/model/player.dart'; 5 | import 'package:snaphunt/stores/hunt_model.dart'; 6 | import 'package:snaphunt/widgets/common/hunt_game.dart'; 7 | import 'package:snaphunt/utils/utils.dart'; 8 | 9 | class MultiPlayer extends StatelessWidget { 10 | final Game game; 11 | final String userId; 12 | final List players; 13 | 14 | const MultiPlayer({ 15 | Key key, 16 | this.userId, 17 | this.players, 18 | this.game, 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return ChangeNotifierProvider( 24 | builder: (_) => HuntModel( 25 | objects: generateHuntObjectsFromList(game.words), 26 | timeLimit: game.gameStartTime.add(Duration(minutes: game.timeLimit)), 27 | isMultiplayer: true, 28 | gameId: game.id, 29 | userId: userId, 30 | timeDuration: game.timeLimit, 31 | ), 32 | child: HuntGame( 33 | title: game.name, 34 | players: players, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/em2/snaphunt/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.em2.snaphunt 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | import android.os.Build 9 | import android.view.ViewTreeObserver 10 | import android.view.WindowManager 11 | class MainActivity: FlutterActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | val flutter_native_splash = true 15 | var originalStatusBarColor = 0 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 17 | originalStatusBarColor = window.statusBarColor 18 | window.statusBarColor = 0xffeaeaea.toInt() 19 | } 20 | val originalStatusBarColorFinal = originalStatusBarColor 21 | 22 | GeneratedPluginRegistrant.registerWith(this) 23 | val vto = flutterView.viewTreeObserver 24 | vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { 25 | override fun onGlobalLayout() { 26 | flutterView.viewTreeObserver.removeOnGlobalLayoutListener(this) 27 | window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 29 | window.statusBarColor = originalStatusBarColorFinal 30 | } 31 | } 32 | }) 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.0' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.3.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:3.2.1' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | // subprojects { 31 | // project.configurations.all { 32 | // resolutionStrategy.eachDependency { details -> 33 | // if (details.requested.group == 'androidx' && 34 | // !details.requested.name.contains('androidx')) { 35 | // details.useVersion "1.0.0" 36 | // } 37 | // } 38 | // } 39 | // } 40 | 41 | // subprojects { 42 | // project.configurations.all { 43 | // resolutionStrategy.eachDependency { details -> 44 | // if (details.requested.group == 'androidx.core' && 45 | // !details.requested.name.contains('androidx')) { 46 | // details.useVersion "1.0.0" 47 | // } 48 | // } 49 | // } 50 | // } 51 | 52 | task clean(type: Delete) { 53 | delete rootProject.buildDir 54 | } 55 | -------------------------------------------------------------------------------- /lib/stores/player_hunt_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:snaphunt/data/repository.dart'; 6 | import 'package:snaphunt/model/player.dart'; 7 | 8 | class PlayHuntModel with ChangeNotifier { 9 | final String gameId; 10 | 11 | final List players; 12 | 13 | PlayHuntModel({this.players, this.gameId}); 14 | 15 | StreamSubscription playerStream; 16 | 17 | final repository = Repository.instance; 18 | 19 | @override 20 | void addListener(listener) { 21 | super.addListener(listener); 22 | initStreams(); 23 | } 24 | 25 | @override 26 | void dispose() { 27 | playerStream.cancel(); 28 | super.dispose(); 29 | } 30 | 31 | void initStreams() { 32 | playerStream = repository.playersSnapshot(gameId).listen(playerListener); 33 | } 34 | 35 | void playerListener(QuerySnapshot snapshot) { 36 | snapshot.documentChanges.forEach((DocumentChange change) async { 37 | if (DocumentChangeType.modified == change.type) { 38 | int index = players.indexWhere( 39 | (player) => player.user.uid == change.document.documentID); 40 | 41 | if (index != -1) { 42 | players[index].score = change.document.data['score']; 43 | sort(); 44 | notifyListeners(); 45 | } 46 | } 47 | }); 48 | } 49 | 50 | void sort() { 51 | players.sort((a, b) => b.score.compareTo(a.score)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | snaphunt 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/model/game.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | class Game { 4 | String id; 5 | String name; 6 | String createdBy; 7 | int maxPlayers; 8 | int timeLimit; 9 | int noOfItems; 10 | String status; 11 | DateTime timeCreated; 12 | DateTime gameStartTime; 13 | List words; 14 | 15 | Game( 16 | {this.id, 17 | this.name, 18 | this.createdBy, 19 | this.maxPlayers, 20 | this.timeLimit, 21 | this.noOfItems, 22 | this.status, 23 | this.timeCreated, 24 | this.gameStartTime, 25 | this.words}); 26 | 27 | Game.fromJson(Map json) { 28 | id = json['id']; 29 | name = json['name']; 30 | createdBy = json['createdBy']; 31 | maxPlayers = json['maxPlayers']; 32 | timeLimit = json['timeLimit']; 33 | noOfItems = json['noOfItems']; 34 | status = json['status']; 35 | timeCreated = DateTime.fromMillisecondsSinceEpoch( 36 | (json['timeCreated'] as Timestamp).millisecondsSinceEpoch); 37 | gameStartTime = json['gameStartTime'] != null 38 | ? DateTime.fromMillisecondsSinceEpoch( 39 | (json['gameStartTime'] as Timestamp).millisecondsSinceEpoch) 40 | : null; 41 | words = json['words']?.cast(); 42 | } 43 | 44 | Map toJson() { 45 | final Map data = new Map(); 46 | data['id'] = this.id; 47 | data['name'] = this.name; 48 | data['createdBy'] = this.createdBy; 49 | data['maxPlayers'] = this.maxPlayers; 50 | data['timeLimit'] = this.timeLimit; 51 | data['noOfItems'] = this.noOfItems; 52 | data['status'] = this.status; 53 | data['timeCreated'] = this.timeCreated; 54 | data['gameStartTime'] = this.gameStartTime; 55 | data['words'] = this.words; 56 | return data; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/widgets/common/card_textfield.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CardTextField extends StatelessWidget { 4 | final String label; 5 | final Widget widget; 6 | 7 | const CardTextField({ 8 | Key key, 9 | this.label, 10 | this.widget, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | margin: const EdgeInsets.symmetric( 17 | horizontal: 8.0, 18 | vertical: 2.0, 19 | ), 20 | height: 60, 21 | child: Card( 22 | shape: RoundedRectangleBorder( 23 | borderRadius: BorderRadius.circular(8.0), 24 | ), 25 | elevation: 3, 26 | child: Container( 27 | margin: EdgeInsets.all(8.0), 28 | child: Row( 29 | mainAxisSize: MainAxisSize.max, 30 | crossAxisAlignment: CrossAxisAlignment.baseline, 31 | textBaseline: TextBaseline.alphabetic, 32 | children: [ 33 | Expanded( 34 | flex: 2, 35 | child: Center( 36 | child: Text( 37 | label, 38 | style: TextStyle( 39 | fontSize: 16, 40 | fontWeight: FontWeight.bold 41 | ), 42 | ), 43 | ), 44 | ), 45 | const VerticalDivider( 46 | width: 10, 47 | thickness: 1, 48 | ), 49 | Expanded( 50 | flex: 5, 51 | child: Container( 52 | child: widget, 53 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 54 | ), 55 | ) 56 | ], 57 | ), 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/widgets/multiplayer/create_buttons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/constants/app_theme.dart'; 3 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 4 | 5 | class CreateButtons extends StatelessWidget { 6 | final Function onCreate; 7 | final String cancelLabel; 8 | final String createLabel; 9 | 10 | const CreateButtons({ 11 | Key key, 12 | this.onCreate, 13 | this.cancelLabel = 'Cancel', 14 | this.createLabel = 'Create Room', 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | margin: const EdgeInsets.symmetric(horizontal: 16.0), 21 | child: Row( 22 | mainAxisSize: MainAxisSize.max, 23 | crossAxisAlignment: CrossAxisAlignment.center, 24 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 25 | children: [ 26 | Expanded( 27 | child: FancyButton( 28 | child: Center( 29 | child: Text( 30 | cancelLabel, 31 | style: fancy_button_style, 32 | ), 33 | ), 34 | color: Colors.blueGrey, 35 | size: 60, 36 | onPressed: () { 37 | Navigator.of(context).pop(); 38 | }, 39 | ), 40 | ), 41 | SizedBox(width: 15), 42 | Expanded( 43 | child: FancyButton( 44 | child: Center( 45 | child: Text( 46 | createLabel, 47 | style: fancy_button_style, 48 | ), 49 | ), 50 | color: Colors.deepOrangeAccent, 51 | size: 60, 52 | onPressed: onCreate, 53 | ), 54 | ), 55 | ], 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | 75 | google-services.json 76 | android/key\.properties 77 | *.jks 78 | -------------------------------------------------------------------------------- /lib/widgets/multiplayer/lobby_buttons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/constants/app_theme.dart'; 3 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 4 | 5 | class LobbyButtons extends StatelessWidget { 6 | final Function onJoinRoom; 7 | final Function onCreateRoom; 8 | 9 | const LobbyButtons({Key key, this.onJoinRoom, this.onCreateRoom}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | margin: const EdgeInsets.fromLTRB(4, 12, 4, 2), 16 | child: Row( 17 | crossAxisAlignment: CrossAxisAlignment.center, 18 | mainAxisAlignment: MainAxisAlignment.start, 19 | children: [ 20 | Expanded( 21 | child: Center( 22 | child: Text( 23 | 'Snap Rooms', 24 | maxLines: 2, 25 | style: TextStyle(fontSize: 28), 26 | ), 27 | ), 28 | ), 29 | Expanded( 30 | child: FancyButton( 31 | child: Center( 32 | child: Text( 33 | 'Join Room', 34 | style: fancy_button_style, 35 | ), 36 | ), 37 | color: Colors.orange, 38 | size: 60, 39 | onPressed: onJoinRoom, 40 | ), 41 | ), 42 | const SizedBox(width: 10), 43 | Expanded( 44 | child: FancyButton( 45 | child: Center( 46 | child: Text( 47 | 'Create Room', 48 | style: fancy_button_style, 49 | ), 50 | ), 51 | color: Colors.red, 52 | size: 60, 53 | onPressed: onCreateRoom, 54 | ), 55 | ), 56 | ], 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hive/hive.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:snaphunt/model/hunt.dart'; 7 | import 'package:snaphunt/widgets/common/fancy_alert_dialog.dart'; 8 | import 'package:flutter/services.dart' show rootBundle; 9 | 10 | void showAlertDialog({BuildContext context, String title, String body}) { 11 | showDialog( 12 | context: context, 13 | barrierDismissible: false, 14 | builder: (BuildContext context) => FancyAlertDialog( 15 | title: title, 16 | body: body, 17 | ), 18 | ); 19 | } 20 | 21 | Future loadAsset() async { 22 | return await rootBundle.loadString('assets/default.json'); 23 | } 24 | 25 | Future openDB() async { 26 | final dir = await getApplicationDocumentsDirectory(); 27 | Hive.init(dir.path); 28 | 29 | return Future.wait([ 30 | Hive.openBox('words'), 31 | ]); 32 | } 33 | 34 | void initDB() async { 35 | final box = Hive.box('words'); 36 | 37 | if (box.isEmpty) { 38 | Map data = json.decode(await loadAsset()); 39 | 40 | box.put('version', data['version']); 41 | box.put('words', data['words']); 42 | } 43 | } 44 | 45 | List generateWords([int numOfWords = 8]) { 46 | final box = Hive.box('words'); 47 | 48 | final List words = box.get('words'); 49 | words.shuffle(); 50 | 51 | return List.generate(numOfWords, (index) => words[index]); 52 | } 53 | 54 | List generateHuntObjects([int numOfWords = 8]) { 55 | final List words = generateWords(numOfWords); 56 | 57 | return new List.generate(numOfWords, (index) { 58 | return Hunt(word: words[index]); 59 | }); 60 | } 61 | 62 | List generateHuntObjectsFromList(List words) { 63 | return new List.generate(words.length, (index) { 64 | return Hunt(word: words[index]); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 21 | 22 | 29 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/widgets/common/fancy_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/constants/app_theme.dart'; 3 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 4 | 5 | class FancyAlertDialog extends StatelessWidget { 6 | final String title; 7 | final String body; 8 | 9 | const FancyAlertDialog({Key key, this.title, this.body}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return AlertDialog( 14 | shape: RoundedRectangleBorder( 15 | borderRadius: BorderRadius.circular(18.0), 16 | ), 17 | elevation: 5, 18 | title: Center( 19 | child: Text( 20 | title, 21 | style: TextStyle( 22 | fontSize: 32, 23 | fontWeight: FontWeight.bold, 24 | ), 25 | ), 26 | ), 27 | contentPadding: const EdgeInsets.fromLTRB(24, 32, 24, 18), 28 | content: Column( 29 | mainAxisSize: MainAxisSize.min, 30 | crossAxisAlignment: CrossAxisAlignment.center, 31 | children: [ 32 | Text( 33 | body, 34 | textAlign: TextAlign.center, 35 | style: TextStyle(fontSize: 20), 36 | ), 37 | const SizedBox(height: 30), 38 | DialogFancyButtonExit( 39 | text: 'OKAY', 40 | color: Colors.orange, 41 | onPressed: () { 42 | Navigator.of(context).pop(); 43 | }, 44 | ) 45 | ], 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | class DialogFancyButtonExit extends StatelessWidget { 52 | final String text; 53 | final Color color; 54 | final Function onPressed; 55 | 56 | const DialogFancyButtonExit({ 57 | Key key, 58 | this.text, 59 | this.color, 60 | this.onPressed, 61 | }) : super(key: key); 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | return FancyButton( 66 | child: Center( 67 | child: Text( 68 | text, 69 | style: fancy_button_style, 70 | ), 71 | ), 72 | size: 60, 73 | color: color, 74 | onPressed: onPressed, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: snaphunt 2 | description: SnapHunt - Flutter Philippines Hackathon 2019 Entry 3 | 4 | version: 1.0.0 5 | 6 | environment: 7 | sdk: ">=2.2.2 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | # firebase 14 | firebase_core: ^0.4.0+9 15 | 16 | # realtime db 17 | cloud_firestore: ^0.12.9+5 18 | 19 | # ml 20 | firebase_ml_vision: ^0.9.2+2 21 | 22 | # auth 23 | firebase_auth: ^0.14.0+5 24 | google_sign_in: ^4.0.7 25 | 26 | # di 27 | provider: ^3.1.0+1 28 | 29 | # qr 30 | qr_flutter: ^3.0.1 31 | flutter_barcode_scanner: ^0.1.7 32 | 33 | # camera feed 34 | camera: ^0.5.6+2 35 | path_provider: ^1.4.0 36 | 37 | # vibrator 38 | vibration: ^1.2.2 39 | 40 | # check connectivity 41 | connectivity: ^0.4.5+2 42 | 43 | # autosize text 44 | auto_size_text: ^2.1.0 45 | 46 | # local db 47 | hive: ^1.1.1 48 | 49 | # screenshot widget 50 | screenshot: ^0.1.1 51 | 52 | # share images 53 | esys_flutter_share: ^1.0.2 54 | 55 | expandable: ^3.0.1 56 | 57 | # wave 58 | wave: ^0.0.8 59 | 60 | # list animations are <3 61 | flutter_staggered_animations: ^0.1.2 62 | 63 | # The following adds the Cupertino Icons font to your application. 64 | # Use with the CupertinoIcons class for iOS style icons. 65 | cupertino_icons: ^0.1.2 66 | 67 | # introduction 68 | intro_views_flutter: '^2.8.0' 69 | 70 | dev_dependencies: 71 | flutter_test: 72 | sdk: flutter 73 | 74 | flutter_native_splash: ^0.1.9 75 | 76 | flutter_native_splash: 77 | image: assets/splash.png 78 | color: "ffffff" 79 | ios: false 80 | 81 | flutter: 82 | uses-material-design: true 83 | 84 | assets: 85 | - assets/default.json 86 | - assets/top.png 87 | - assets/main.png 88 | - assets/trophy.png 89 | - assets/intro_one.png 90 | - assets/intro_two.png 91 | - assets/intro_three.png 92 | - assets/intro_four.png 93 | - assets/intro_five.png 94 | - assets/intro_six.png 95 | 96 | fonts: 97 | - family: SF_Atarian_System 98 | fonts: 99 | - asset: assets/fonts/SF_Atarian_System.ttf 100 | - asset: assets/fonts/SF_Atarian_System_Bold.ttf 101 | weight: 700 102 | -------------------------------------------------------------------------------- /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/widgets/common/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingTextField extends StatefulWidget { 4 | @override 5 | _LoadingTextFieldState createState() => _LoadingTextFieldState(); 6 | } 7 | 8 | class _LoadingTextFieldState extends State 9 | with SingleTickerProviderStateMixin { 10 | AnimationController _controller; 11 | Animation animation; 12 | 13 | @override 14 | void initState() { 15 | super.initState(); 16 | _controller = AnimationController( 17 | vsync: this, 18 | duration: Duration(seconds: 1), 19 | ); 20 | 21 | animation = Tween(begin: -1.0, end: 2.0).animate( 22 | CurvedAnimation(curve: Curves.easeInOutSine, parent: _controller)); 23 | 24 | animation.addStatusListener((status) { 25 | if (status == AnimationStatus.completed || 26 | status == AnimationStatus.dismissed) { 27 | _controller.repeat(); 28 | } else if (status == AnimationStatus.dismissed) { 29 | _controller.forward(); 30 | } 31 | }); 32 | _controller.forward(); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _controller.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | var brightness = Theme.of(context).brightness; 44 | 45 | return AnimatedBuilder( 46 | animation: animation, 47 | builder: (context, child) { 48 | return Padding( 49 | padding: const EdgeInsets.all(1.0), 50 | child: Container( 51 | width: 75, 52 | height: 8.0, 53 | decoration: myBoxDec(animation, brightness: brightness), 54 | ), 55 | ); 56 | }, 57 | ); 58 | } 59 | } 60 | 61 | Decoration myBoxDec(animation, {isCircle = false, brightness}) { 62 | final dark = [ 63 | Colors.grey[700], 64 | Colors.grey[600], 65 | Colors.grey[700], 66 | ]; 67 | 68 | final light = [ 69 | Color(0xfff6f7f9), 70 | Color(0xffe9ebee), 71 | Color(0xfff6f7f9), 72 | ]; 73 | return BoxDecoration( 74 | shape: isCircle ? BoxShape.circle : BoxShape.rectangle, 75 | gradient: LinearGradient( 76 | begin: Alignment.centerLeft, 77 | end: Alignment.centerRight, 78 | colors: brightness == Brightness.light ? light : dark, 79 | stops: [ 80 | // animation.value * 0.1, 81 | animation.value - 1, 82 | animation.value, 83 | animation.value + 1, 84 | // animation.value + 5, 85 | // 1.0, 86 | ], 87 | ), 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /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 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | 29 | def keystoreProperties = new Properties() 30 | def keystorePropertiesFile = rootProject.file('key.properties') 31 | if (keystorePropertiesFile.exists()) { 32 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 33 | } 34 | 35 | android { 36 | compileSdkVersion 28 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | 42 | lintOptions { 43 | disable 'InvalidPackage' 44 | } 45 | 46 | defaultConfig { 47 | applicationId "com.em2.snaphunt" 48 | minSdkVersion 21 49 | targetSdkVersion 28 50 | multiDexEnabled true 51 | versionCode flutterVersionCode.toInteger() 52 | versionName flutterVersionName 53 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 54 | } 55 | 56 | signingConfigs { 57 | release { 58 | keyAlias keystoreProperties['keyAlias'] 59 | keyPassword keystoreProperties['keyPassword'] 60 | storeFile file(keystoreProperties['storeFile']) 61 | storePassword keystoreProperties['storePassword'] 62 | } 63 | } 64 | buildTypes { 65 | release { 66 | signingConfig signingConfigs.release 67 | } 68 | } 69 | 70 | } 71 | 72 | flutter { 73 | source '../..' 74 | } 75 | 76 | dependencies { 77 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 78 | androidTestImplementation 'androidx.test:runner:1.1.0' 79 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 80 | 81 | api 'com.google.firebase:firebase-ml-vision-image-label-model:17.0.2' 82 | } 83 | 84 | apply plugin: 'com.google.gms.google-services' 85 | -------------------------------------------------------------------------------- /lib/widgets/common/countdown.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CountDownTimer extends StatefulWidget { 4 | final DateTime timeLimit; 5 | 6 | const CountDownTimer({Key key, this.timeLimit}) : super(key: key); 7 | 8 | State createState() => new _CountDownTimerState(); 9 | } 10 | 11 | class _CountDownTimerState extends State 12 | with TickerProviderStateMixin { 13 | int secondsRemaining; 14 | AnimationController _controller; 15 | Duration duration; 16 | 17 | String get timerDisplayString { 18 | Duration duration = _controller.duration * _controller.value; 19 | return formatHHMMSS(duration.inSeconds); 20 | } 21 | 22 | String formatHHMMSS(int seconds) { 23 | int hours = (seconds / 3600).truncate(); 24 | seconds = (seconds % 3600).truncate(); 25 | int minutes = (seconds / 60).truncate(); 26 | 27 | String hoursStr = (hours).toString().padLeft(2, '0'); 28 | String minutesStr = (minutes).toString().padLeft(2, '0'); 29 | String secondsStr = (seconds % 60).toString().padLeft(2, '0'); 30 | 31 | if (hours == 0) { 32 | return "$minutesStr:$secondsStr"; 33 | } 34 | 35 | return "$hoursStr:$minutesStr:$secondsStr"; 36 | } 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | final now = DateTime.now(); 42 | 43 | secondsRemaining = widget.timeLimit.difference(now).inSeconds; 44 | duration = new Duration(seconds: secondsRemaining); 45 | _controller = new AnimationController( 46 | vsync: this, 47 | duration: duration, 48 | ); 49 | _controller.reverse(from: secondsRemaining.toDouble()); 50 | } 51 | 52 | @override 53 | void didUpdateWidget(CountDownTimer oldWidget) { 54 | super.didUpdateWidget(oldWidget); 55 | 56 | if (secondsRemaining != secondsRemaining) { 57 | setState(() { 58 | duration = new Duration(seconds: secondsRemaining); 59 | _controller.dispose(); 60 | _controller = new AnimationController( 61 | vsync: this, 62 | duration: duration, 63 | ); 64 | _controller.reverse(from: secondsRemaining.toDouble()); 65 | }); 66 | } 67 | } 68 | 69 | @override 70 | void dispose() { 71 | _controller.dispose(); 72 | super.dispose(); 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return Center( 78 | child: AnimatedBuilder( 79 | animation: _controller, 80 | builder: (_, Widget child) { 81 | return Text( 82 | timerDisplayString, 83 | style: TextStyle( 84 | fontSize: 24, 85 | fontWeight: FontWeight.w600, 86 | color: Colors.white, 87 | ), 88 | ); 89 | }), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/widgets/multiplayer/room_exit_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/constants/app_theme.dart'; 3 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 4 | 5 | class RoomExitDialog extends StatelessWidget { 6 | final String title; 7 | final String body; 8 | 9 | const RoomExitDialog({Key key, this.title, this.body}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return AlertDialog( 14 | shape: RoundedRectangleBorder( 15 | borderRadius: BorderRadius.circular(18.0), 16 | ), 17 | elevation: 5, 18 | title: Center( 19 | child: Text( 20 | title, 21 | style: TextStyle( 22 | fontSize: 32, 23 | fontWeight: FontWeight.bold, 24 | ), 25 | ), 26 | ), 27 | contentPadding: const EdgeInsets.fromLTRB(24, 32, 24, 18), 28 | content: Column( 29 | mainAxisSize: MainAxisSize.min, 30 | crossAxisAlignment: CrossAxisAlignment.center, 31 | children: [ 32 | const SizedBox(height: 20), 33 | Text( 34 | body, 35 | textAlign: TextAlign.center, 36 | style: TextStyle(fontSize: 24, height: 0.9), 37 | ), 38 | const SizedBox(height: 40), 39 | Row( 40 | mainAxisSize: MainAxisSize.max, 41 | children: [ 42 | Expanded( 43 | child: DialogFancyButtonExit( 44 | text: 'OKAY', 45 | color: Colors.orange, 46 | onPressed: () { 47 | Navigator.of(context).pop(true); 48 | }, 49 | ), 50 | ), 51 | SizedBox(width: 10), 52 | Expanded( 53 | child: DialogFancyButtonExit( 54 | text: 'CANCEL', 55 | color: Colors.blueGrey, 56 | onPressed: () { 57 | Navigator.of(context).pop(false); 58 | }, 59 | ), 60 | ) 61 | ], 62 | ), 63 | ], 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | class DialogFancyButtonExit extends StatelessWidget { 70 | final String text; 71 | final Color color; 72 | final Function onPressed; 73 | 74 | const DialogFancyButtonExit({ 75 | Key key, 76 | this.text, 77 | this.color, 78 | this.onPressed, 79 | }) : super(key: key); 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return FancyButton( 84 | child: Center( 85 | child: Text( 86 | text, 87 | style: fancy_button_style, 88 | ), 89 | ), 90 | size: 60, 91 | color: color, 92 | onPressed: onPressed, 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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/ui/singleplayer/single_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/model/hunt.dart'; 3 | import 'package:snaphunt/ui/home.dart'; 4 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 5 | import 'package:snaphunt/widgets/common/wave.dart'; 6 | 7 | class ResultScreenSinglePlayer extends StatelessWidget { 8 | final bool isHuntFinished; 9 | final List objects; 10 | final Duration duration; 11 | 12 | const ResultScreenSinglePlayer( 13 | {Key key, this.isHuntFinished, this.objects, this.duration}) 14 | : super(key: key); 15 | 16 | String formatHHMMSS(int seconds) { 17 | int hours = (seconds / 3600).truncate(); 18 | seconds = (seconds % 3600).truncate(); 19 | int minutes = (seconds / 60).truncate(); 20 | 21 | String hoursStr = (hours).toString().padLeft(2, '0'); 22 | String minutesStr = (minutes).toString().padLeft(2, '0'); 23 | String secondsStr = (seconds % 60).toString().padLeft(2, '0'); 24 | 25 | if (hours == 0) { 26 | return "$minutesStr:$secondsStr"; 27 | } 28 | 29 | return "$hoursStr:$minutesStr:$secondsStr"; 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | final found = objects.where((hunt) => hunt.isFound).length; 35 | return Scaffold( 36 | body: Container( 37 | color: Colors.white, 38 | height: double.infinity, 39 | child: Column( 40 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 41 | children: [ 42 | Image.asset('assets/top.png', scale: 1), 43 | // Image.asset('assets/main.png', height: 185), 44 | 45 | const SizedBox(height: 15), 46 | const UserInfo(), 47 | const SizedBox(height: 20), 48 | Text( 49 | isHuntFinished 50 | ? 'Congrats! You found all items' 51 | : 'Better luck next time!', 52 | maxLines: 2, 53 | style: TextStyle( 54 | fontSize: 32, 55 | fontWeight: FontWeight.w300, 56 | ), 57 | ), 58 | const SizedBox(height: 25), 59 | Text( 60 | '$found/${objects.length} found', 61 | style: TextStyle( 62 | fontSize: 24, 63 | fontWeight: FontWeight.w300, 64 | ), 65 | ), 66 | Text( 67 | 'in ${formatHHMMSS(duration.inSeconds)}!', 68 | style: TextStyle( 69 | fontSize: 24, 70 | fontWeight: FontWeight.w300, 71 | ), 72 | ), 73 | const SizedBox(height: 20), 74 | FancyButton( 75 | child: Text( 76 | "Back", 77 | style: TextStyle( 78 | color: Colors.white, 79 | fontSize: 18, 80 | ), 81 | ), 82 | size: 70, 83 | color: Colors.blue, 84 | onPressed: () { 85 | Navigator.of(context).pop(); 86 | }, 87 | ), 88 | CustomWaveWidget() 89 | ], 90 | )), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:camera/camera.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:snaphunt/data/repository.dart'; 7 | import 'package:snaphunt/routes.dart'; 8 | import 'package:snaphunt/services/auth.dart'; 9 | import 'package:snaphunt/services/connectivity.dart'; 10 | import 'package:snaphunt/ui/home.dart'; 11 | import 'package:snaphunt/ui/login.dart'; 12 | import 'package:snaphunt/utils/utils.dart'; 13 | import 'package:snaphunt/widgets/common/custom_scroll.dart'; 14 | 15 | List cameras; 16 | 17 | Future main() async { 18 | WidgetsFlutterBinding.ensureInitialized(); 19 | SystemChrome.setPreferredOrientations([ 20 | DeviceOrientation.portraitUp, 21 | DeviceOrientation.portraitDown, 22 | ]); 23 | 24 | cameras = await availableCameras(); 25 | openDB().then((_) async { 26 | initDB(); 27 | runApp(App(auth: await Auth.create())); 28 | }); 29 | } 30 | 31 | class App extends StatefulWidget { 32 | const App({ 33 | Key key, 34 | @required this.auth, 35 | }) : super(key: key); 36 | 37 | final Auth auth; 38 | 39 | @override 40 | _AppState createState() => _AppState(); 41 | } 42 | 43 | class _AppState extends State { 44 | final _navigatorKey = GlobalKey(); 45 | FirebaseUser currentUser; 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | Repository.instance.updateLocalWords(); 51 | currentUser = widget.auth.init(_onUserChanged); 52 | } 53 | 54 | void _onUserChanged() { 55 | final user = widget.auth.currentUser.value; 56 | 57 | if (currentUser == null && user != null) { 58 | Repository.instance.updateUserData(user); 59 | _navigatorKey.currentState 60 | .pushAndRemoveUntil(Home.route(), (route) => false); 61 | } else if (currentUser != null && user == null) { 62 | _navigatorKey.currentState 63 | .pushAndRemoveUntil(Login.route(), (route) => false); 64 | } 65 | currentUser = user; 66 | } 67 | 68 | @override 69 | void dispose() { 70 | widget.auth.dispose(_onUserChanged); 71 | super.dispose(); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return MultiProvider( 77 | providers: [ 78 | Provider.value(value: widget.auth), 79 | ValueListenableProvider.value( 80 | value: widget.auth.currentUser), 81 | StreamProvider.controller( 82 | builder: (context) => 83 | ConnectivityService().connectionStatusController, 84 | ) 85 | ], 86 | child: MaterialApp( 87 | title: 'SnapHunt', 88 | theme: ThemeData( 89 | primaryColor: Colors.orange, 90 | textTheme: TextTheme(), 91 | fontFamily: 'SF_Atarian_System', 92 | ), 93 | navigatorKey: _navigatorKey, 94 | onGenerateRoute: Router.generateRoute, 95 | builder: (context, child) { 96 | return ScrollConfiguration( 97 | behavior: NoOverFlowScrollBehavior(), 98 | child: child, 99 | ); 100 | }, 101 | home: currentUser == null ? const Login() : const Home(), 102 | ), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/ui/singleplayer/single_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hive/hive.dart'; 3 | import 'package:snaphunt/routes.dart'; 4 | import 'package:snaphunt/widgets/common/card_textfield.dart'; 5 | import 'package:snaphunt/widgets/multiplayer/create_buttons.dart'; 6 | 7 | class SinglePlayerSettings extends StatefulWidget { 8 | @override 9 | _SinglePlayerSettingsState createState() => _SinglePlayerSettingsState(); 10 | } 11 | 12 | class _SinglePlayerSettingsState extends State { 13 | final itemsController = TextEditingController(); 14 | int dropdownValue = 3; 15 | 16 | @override 17 | void initState() { 18 | itemsController.text = '8'; 19 | super.initState(); 20 | } 21 | 22 | @override 23 | void dispose() { 24 | itemsController.dispose(); 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final objectCount = Hive.box('words').get('words').length; 31 | 32 | return Scaffold( 33 | resizeToAvoidBottomPadding: false, 34 | appBar: AppBar( 35 | title: Text( 36 | 'Single player', 37 | style: TextStyle(color: Colors.white), 38 | ), 39 | leading: Container(), 40 | centerTitle: true, 41 | elevation: 0, 42 | ), 43 | body: ListView( 44 | physics: NeverScrollableScrollPhysics(), 45 | children: [ 46 | const SizedBox(height: 10), 47 | CardTextField( 48 | label: 'Time Limit', 49 | widget: DropdownButton( 50 | isExpanded: true, 51 | value: dropdownValue, 52 | iconSize: 24, 53 | elevation: 16, 54 | underline: Container(), 55 | onChanged: (newVal) { 56 | setState(() { 57 | dropdownValue = newVal; 58 | }); 59 | }, 60 | items: [3, 5, 8, 12, 15] 61 | .map>((int value) { 62 | return DropdownMenuItem( 63 | value: value, 64 | child: Text( 65 | '$value mins', 66 | style: TextStyle( 67 | fontSize: 16, 68 | ), 69 | ), 70 | ); 71 | }).toList(), 72 | ), 73 | ), 74 | CardTextField( 75 | label: 'No. of Items', 76 | widget: TextField( 77 | controller: itemsController, 78 | keyboardType: TextInputType.number, 79 | maxLines: 1, 80 | decoration: InputDecoration( 81 | border: InputBorder.none, 82 | ), 83 | ), 84 | ), 85 | const SizedBox(height: 20), 86 | CreateButtons( 87 | createLabel: 'Play!', 88 | onCreate: () { 89 | int numObjects = int.parse(itemsController.text); 90 | 91 | if (numObjects == 0) { 92 | numObjects = 1; 93 | } else if (numObjects > objectCount) { 94 | numObjects = objectCount; 95 | } 96 | 97 | Navigator.of(context).pushReplacementNamed( 98 | Router.singlePlayer, 99 | arguments: [ 100 | numObjects, 101 | dropdownValue, 102 | ], 103 | ); 104 | }, 105 | ) 106 | ], 107 | ), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:snaphunt/stores/game_model.dart'; 4 | import 'package:snaphunt/ui/home.dart'; 5 | import 'package:snaphunt/ui/login.dart'; 6 | import 'package:snaphunt/ui/how_to_play.dart'; 7 | import 'package:snaphunt/ui/multiplayer/create_room.dart'; 8 | import 'package:snaphunt/ui/multiplayer/lobby.dart'; 9 | import 'package:snaphunt/ui/multiplayer/multiplayer.dart'; 10 | import 'package:snaphunt/ui/multiplayer/multiplayer_result.dart'; 11 | import 'package:snaphunt/ui/multiplayer/room.dart'; 12 | import 'package:snaphunt/ui/singleplayer/single_result.dart'; 13 | import 'package:snaphunt/ui/singleplayer/single_settings.dart'; 14 | import 'package:snaphunt/ui/singleplayer/singleplayer.dart'; 15 | 16 | class Router { 17 | static const String home = '/'; 18 | 19 | static const String login = '/login'; 20 | static const String howToPlay = '/howToPlay'; 21 | 22 | static const String lobby = '/multiplayer'; 23 | static const String create = '/createRoom'; 24 | static const String room = '/room'; 25 | static const String game = '/game'; 26 | static const String resultMulti = '/multiplayerResult'; 27 | 28 | static const String singlePlayerSettings = '/singleplayerSettings'; 29 | static const String singlePlayer = '/singleplayer'; 30 | static const String resultSingle = '/singleplayerResult'; 31 | 32 | static Route generateRoute(RouteSettings settings) { 33 | switch (settings.name) { 34 | case home: 35 | return MaterialPageRoute(builder: (_) => Home()); 36 | 37 | case lobby: 38 | return MaterialPageRoute(builder: (_) => Lobby()); 39 | 40 | case create: 41 | return MaterialPageRoute(builder: (_) => CreateRoom()); 42 | 43 | case game: 44 | final args = settings.arguments as List; 45 | 46 | return MaterialPageRoute( 47 | builder: (_) => MultiPlayer( 48 | game: args[0], 49 | userId: args[1], 50 | players: args[2], 51 | ), 52 | ); 53 | 54 | case room: 55 | final args = settings.arguments as List; 56 | 57 | return MaterialPageRoute( 58 | builder: (_) => ChangeNotifierProvider( 59 | builder: (_) => GameModel( 60 | args[0], 61 | args[1], 62 | args[2], 63 | ), 64 | child: Room(), 65 | ), 66 | ); 67 | 68 | case resultMulti: 69 | final args = settings.arguments as List; 70 | 71 | return MaterialPageRoute( 72 | builder: (_) => ResultMultiPlayer( 73 | gameId: args[0], 74 | title: args[1], 75 | duration: args[2], 76 | ), 77 | ); 78 | 79 | case singlePlayerSettings: 80 | return MaterialPageRoute(builder: (_) => SinglePlayerSettings()); 81 | 82 | case singlePlayer: 83 | final args = settings.arguments as List; 84 | 85 | return MaterialPageRoute( 86 | builder: (_) => SinglePlayer( 87 | numOfObjects: args[0], 88 | duration: args[1], 89 | ), 90 | ); 91 | 92 | case resultSingle: 93 | final args = settings.arguments as List; 94 | 95 | return MaterialPageRoute( 96 | builder: (_) => ResultScreenSinglePlayer( 97 | isHuntFinished: args[0], 98 | objects: args[1], 99 | duration: args[2], 100 | ), 101 | ); 102 | 103 | case howToPlay: 104 | return MaterialPageRoute(builder: (_) => HowToPlay()); 105 | 106 | case login: 107 | default: 108 | return MaterialPageRoute(builder: (_) => Login()); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/services/auth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:google_sign_in/google_sign_in.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | // src: https://gist.github.com/slightfoot/6f97d6c1ec4eb52ce880c6394adb1386 10 | class Auth { 11 | static Future create() async { 12 | final currentUser = await FirebaseAuth.instance.currentUser(); 13 | return Auth._(currentUser); 14 | } 15 | 16 | static Auth of(BuildContext context) { 17 | return Provider.of(context, listen: false); 18 | } 19 | 20 | Auth._( 21 | FirebaseUser currentUser, 22 | ) : this.currentUser = ValueNotifier(currentUser); 23 | 24 | final ValueNotifier currentUser; 25 | 26 | final _googleSignIn = GoogleSignIn(); 27 | final _firebaseAuth = FirebaseAuth.instance; 28 | StreamSubscription _authSub; 29 | 30 | FirebaseUser init(VoidCallback onUserChanged) { 31 | currentUser.addListener(onUserChanged); 32 | _authSub = _firebaseAuth.onAuthStateChanged.listen((FirebaseUser user) { 33 | currentUser.value = user; 34 | }); 35 | return currentUser.value; 36 | } 37 | 38 | void dispose(VoidCallback onUserChanged) { 39 | currentUser.removeListener(onUserChanged); 40 | _authSub.cancel(); 41 | } 42 | 43 | Future loginWithEmailAndPassword(String email, String password) async { 44 | try { 45 | await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password); 46 | } catch (e, st) { 47 | throw _getAuthException(e, st); 48 | } 49 | } 50 | 51 | Future loginWithGoogle() async { 52 | try { 53 | final account = await _googleSignIn.signIn(); 54 | if (account == null) { 55 | throw AuthException.cancelled; 56 | } 57 | final auth = await account.authentication; 58 | await _firebaseAuth.signInWithCredential( 59 | GoogleAuthProvider.getCredential(idToken: auth.idToken, accessToken: auth.accessToken), 60 | ); 61 | } catch (e, st) { 62 | throw _getAuthException(e, st); 63 | } 64 | } 65 | 66 | Future logout() async { 67 | try { 68 | await _firebaseAuth.signOut(); 69 | await _googleSignIn.signOut(); 70 | } catch (e, st) { 71 | FlutterError.reportError(FlutterErrorDetails(exception: e, stack: st)); 72 | } 73 | } 74 | 75 | AuthException _getAuthException(dynamic e, StackTrace st) { 76 | if (e is AuthException) { 77 | return e; 78 | } 79 | FlutterError.reportError(FlutterErrorDetails(exception: e, stack: st)); 80 | if (e is PlatformException) { 81 | switch (e.code) { 82 | case 'ERROR_INVALID_EMAIL': 83 | throw const AuthException('Please check your email address.'); 84 | case 'ERROR_WRONG_PASSWORD': 85 | throw const AuthException('Please check your password.'); 86 | case 'ERROR_USER_NOT_FOUND': 87 | throw const AuthException('User not found. Is that the correct email address?'); 88 | case 'ERROR_USER_DISABLED': 89 | throw const AuthException('Your account has been disabled. Please contact support'); 90 | case 'ERROR_TOO_MANY_REQUESTS': 91 | throw const AuthException('You have tried to login too many times. Please try again later.'); 92 | } 93 | } 94 | throw const AuthException('Sorry, an error occurred. Please try again.'); 95 | } 96 | } 97 | 98 | class AuthException implements Exception { 99 | static const cancelled = AuthException('cancelled'); 100 | 101 | const AuthException(this.message); 102 | 103 | final String message; 104 | 105 | @override 106 | String toString() => message; 107 | } -------------------------------------------------------------------------------- /lib/stores/game_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:snaphunt/constants/game_status_enum.dart'; 6 | import 'package:snaphunt/data/repository.dart'; 7 | import 'package:snaphunt/model/game.dart'; 8 | import 'package:snaphunt/model/player.dart'; 9 | 10 | class GameModel with ChangeNotifier { 11 | Game _game; 12 | Game get game => _game; 13 | 14 | bool _isHost; 15 | bool get isHost => _isHost; 16 | 17 | String _userId; 18 | String get userId => _userId; 19 | 20 | bool _isLoading = false; 21 | bool get isLoading => _isLoading; 22 | 23 | GameStatus _status = GameStatus.waiting; 24 | GameStatus get status => _status; 25 | 26 | bool _isGameStart = false; 27 | bool get isGameStart => _isGameStart; 28 | 29 | bool _canStartGame = false; 30 | bool get canStartGame => _canStartGame; 31 | 32 | StreamSubscription gameStream; 33 | StreamSubscription playerStream; 34 | 35 | List _players = []; 36 | List get players => _players; 37 | 38 | final repository = Repository.instance; 39 | 40 | GameModel(this._game, this._isHost, this._userId); 41 | 42 | @override 43 | void addListener(listener) { 44 | initRoom(); 45 | super.addListener(listener); 46 | } 47 | 48 | void initRoom() async { 49 | _isLoading = true; 50 | notifyListeners(); 51 | 52 | if (_isHost) { 53 | String id = await repository.createRoom(_game); 54 | _game.id = id; 55 | } 56 | 57 | await joinRoom(); 58 | 59 | _isLoading = false; 60 | notifyListeners(); 61 | 62 | initStreams(); 63 | } 64 | 65 | @override 66 | void dispose() { 67 | onDispose(); 68 | super.dispose(); 69 | } 70 | 71 | void initStreams() { 72 | gameStream = repository.gameSnapshot(_game.id).listen(gameStatusListener); 73 | playerStream = repository.playersSnapshot(_game.id).listen(playerListener); 74 | } 75 | 76 | void gameStatusListener(DocumentSnapshot snapshot) async { 77 | final status = snapshot.data['status']; 78 | if (status == 'cancelled') { 79 | _status = GameStatus.cancelled; 80 | notifyListeners(); 81 | } else if (status == 'in_game') { 82 | _status = GameStatus.game; 83 | _game = Game.fromJson(snapshot.data); 84 | _game.id = snapshot.documentID; 85 | notifyListeners(); 86 | } 87 | } 88 | 89 | void playerListener(QuerySnapshot snapshot) { 90 | snapshot.documentChanges.forEach((DocumentChange change) async { 91 | print(change.type); 92 | print(change.document.documentID); 93 | 94 | if (DocumentChangeType.added == change.type) { 95 | players.add( 96 | Player(user: await repository.getUser(change.document.documentID))); 97 | } else if (DocumentChangeType.removed == change.type) { 98 | if (change.document.documentID == _userId) { 99 | _status = GameStatus.kicked; 100 | notifyListeners(); 101 | return; 102 | } 103 | 104 | players.removeWhere( 105 | (player) => player.user.uid == change.document.documentID); 106 | } 107 | checkCanStartGame(); 108 | notifyListeners(); 109 | }); 110 | } 111 | 112 | void checkCanStartGame() { 113 | if (players.length >= 2) { 114 | _canStartGame = true; 115 | } else { 116 | _canStartGame = false; 117 | } 118 | } 119 | 120 | void onKickPlayer(String userId) { 121 | repository.kickPlayer(_game.id, userId); 122 | } 123 | 124 | void onDispose() { 125 | if (GameStatus.game != _status) { 126 | if (_isHost) { 127 | repository.cancelRoom(_game.id); 128 | } else { 129 | repository.leaveRoom(_game.id, _userId); 130 | } 131 | } 132 | 133 | gameStream.cancel(); 134 | playerStream.cancel(); 135 | } 136 | 137 | void onGameStart() { 138 | if (_canStartGame) { 139 | _isGameStart = true; 140 | repository.startGame(_game.id, numOfItems: _game.noOfItems); 141 | } 142 | } 143 | 144 | Future joinRoom() async { 145 | final playerCount = await repository.getGamePlayerCount(_game.id); 146 | 147 | if (playerCount >= _game.maxPlayers) { 148 | _status = GameStatus.full; 149 | return Future.value(null); 150 | } 151 | 152 | return repository.joinRoom(_game.id, _userId); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/ui/how_to_play.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intro_views_flutter/Models/page_view_model.dart'; 3 | import 'package:intro_views_flutter/intro_views_flutter.dart'; 4 | 5 | class HowToPlay extends StatelessWidget { 6 | final pages = [ 7 | PageViewModel( 8 | pageColor: Colors.amber, 9 | body: Text('Modes:\nSINGLEPLAYER or MULTIPLAYER', style: TextStyle(fontSize: 24, height: 1)), 10 | title: Text( 11 | 'Select Game Mode', 12 | style: TextStyle( 13 | fontWeight: FontWeight.bold, 14 | ), 15 | ), 16 | textStyle: TextStyle(color: Colors.white), 17 | mainImage: Image.asset( 18 | 'assets/intro_one.png', 19 | height: 400, 20 | width: 400, 21 | alignment: Alignment.center, 22 | ), 23 | ), 24 | PageViewModel( 25 | pageColor: Colors.lightBlue, 26 | body: Text('Set TIME LIMIT and NO. OF ITEMS\n\nMultiplayer:\n(ROOM NAME and MAX PLAYERS)', style: TextStyle(fontSize: 24, height: 1)), 27 | title: Text( 28 | 'Set Game Settings', 29 | style: TextStyle( 30 | fontWeight: FontWeight.bold, 31 | ), 32 | ), 33 | textStyle: TextStyle(color: Colors.white), 34 | mainImage: Image.asset( 35 | 'assets/intro_two.png', 36 | height: 350, 37 | width: 350, 38 | alignment: Alignment.center, 39 | ), 40 | ), 41 | PageViewModel( 42 | pageColor: Colors.deepPurple, 43 | body: Text('Start the game and look for the items displayed on the screen.', style: TextStyle(fontSize: 24, height: 1)), 44 | title: Text( 45 | 'Begin Hunt', 46 | style: TextStyle( 47 | fontWeight: FontWeight.bold, 48 | ), 49 | ), 50 | textStyle: TextStyle(color: Colors.white), 51 | mainImage: Image.asset( 52 | 'assets/intro_three.png', 53 | height: 400, 54 | width: 400, 55 | alignment: Alignment.center, 56 | ), 57 | ), 58 | PageViewModel( 59 | pageColor: Colors.green, 60 | body: Text('Take a snap of the object to be verified.\nEvery point is gained once item is valid.', style: TextStyle(fontSize: 26, height: 1)), 61 | title: Text( 62 | 'Take a Snap', 63 | style: TextStyle( 64 | fontWeight: FontWeight.bold, 65 | ), 66 | ), 67 | textStyle: TextStyle(color: Colors.white), 68 | mainImage: Image.asset( 69 | 'assets/intro_four.png', 70 | height: 400, 71 | width: 400, 72 | alignment: Alignment.center, 73 | ), 74 | ), 75 | PageViewModel( 76 | pageColor: Colors.blueAccent, 77 | body: Text('First one to snap all items or with the highest score before the time limit ends wins the game.', style: TextStyle(fontSize: 26, height: 1)), 78 | title: Text( 79 | 'Be The Champion', 80 | style: TextStyle( 81 | fontWeight: FontWeight.bold, 82 | ), 83 | ), 84 | textStyle: TextStyle(color: Colors.white), 85 | mainImage: Image.asset( 86 | 'assets/intro_five.png', 87 | height: 400, 88 | width: 400, 89 | alignment: Alignment.center, 90 | ), 91 | ), 92 | PageViewModel( 93 | pageColor: Colors.red, 94 | body: Text('You are now ready to begin your Scavenger Game Hunt!', style: TextStyle(fontSize: 26, height: 1)), 95 | title: Text( 96 | 'Let the Hunt Begin', 97 | style: TextStyle( 98 | fontWeight: FontWeight.bold, 99 | ), 100 | ), 101 | textStyle: TextStyle(color: Colors.white), 102 | mainImage: Image.asset( 103 | 'assets/intro_six.png', 104 | height: 320, 105 | width: 320, 106 | alignment: Alignment.center, 107 | ), 108 | ) 109 | ]; 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | return WillPopScope( 114 | onWillPop: () async => false, 115 | child: IntroViewsFlutter( 116 | pages, 117 | onTapDoneButton: () { 118 | Navigator.pop(context); 119 | }, 120 | columnMainAxisAlignment: MainAxisAlignment.center, 121 | fullTransition: 175, 122 | showSkipButton: false, 123 | showNextButton: false, 124 | pageButtonTextStyles: TextStyle( 125 | color: Colors.white, 126 | fontSize: 16.0, 127 | ), 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # SnapHunt 3 | 4 |
5 |

6 | Logo 7 | 8 |

9 | 10 | Get it on Google Play 11 | 12 | SnapHunt is a scavenger hunt game where a player can invite other players on a real world scavenger hunt while using the app as the item identifier by taking a picture of it. Using machine learning technology, the app identifies and scores the player according. Users can play solo or with a group. First player to complete the scavenger hunt or the player the the highest score after countdown ends wins. 13 | 14 | A Flutter PH Hackathon 2019 Entry! 15 | 16 | ## Features 17 | * Single player Scavenger hunt 18 | * Multiplayer Scavenger hunt 19 | * Login via Google Authentication 20 | * Offline Machine learning using ML Kit 21 | * Multiplayer games using Firestore 22 | * Join room via QR code 23 | * Real time score updates in-game 24 | * Pre defined words using Hive 25 | * Update words via Firestore console 26 | * Share results with your friends 27 | 28 | ## Screenshots 29 |

30 | 31 | 32 | 33 |

34 | 35 | ## Download & Install 36 | 37 | You may install SnapHunt via [PlayStore](https://play.google.com/store/apps/details?id=com.em2.snaphunt), installing the apk from [the release section](https://github.com/snap-hunt/snaphunt/releases), or build the app yourself. 38 | 39 | ## Getting Started 40 | 41 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 42 | 43 | ### Prerequisites 44 | 45 | Download either Android Studio or Visual Studio Code, with their respective [Flutter editor plugins](https://flutter.io/get-started/editor/). For more information about Flutter installation procedure, check the [official install guide](https://flutter.io/get-started/install/). 46 | 47 | ### Steps 48 | 1. Clone the repository with the 'clone' command, or just download the zip. 49 | 50 | ``` 51 | $ git clone https://github.com/snap-hunt/snaphunt.git 52 | ``` 53 | 54 | 2. Install dependencies from pubspec.yaml by running `flutter packages get` from the project root (see [using packages documentation](https://flutter.io/using-packages/#adding-a-package-dependency-to-an-app) for details and how to do this in the editor). 55 | 56 | 57 | 3. Follow [Option 1 instructions here up to Step 3](https://firebase.google.com/docs/android/setup#console). Be sure to configure your SHA-1 or SHA-256 hash in the Firebase Project Settings for your app. 58 | 59 | 4. Place the downloaded 'google-services.json' file from Step 1 above in your 60 | projects /android/app/ directory. 61 | 62 | 5. Go to the Firebase Console and then to the Authentication section and then 63 | on to the "Sign-in method" tab an enable Email/Password and Google Sign in methods. 64 | 6. Configure Firestore in your Firebase console 65 | 66 | 7. Add `words` collection with `words` document side. 67 | 68 | 8. Inside the `words` document, add `version` with int value 1. And `words` array with the words the app with be using. 69 | 70 | 9. Build the app! 71 | 72 | ## Built With 73 | 74 | * [Flutter](https://flutter.dev/) - <3 75 | * [Firestore](https://firebase.google.com/docs/firestore) - Realtime Database 76 | * [Firebase Auth](https://firebase.google.com/docs/auth) - Google Authentication 77 | * [ML Kit](https://firebase.google.com/docs/ml-kit) - Machine learning 78 | * [Hive](https://docs.hivedb.dev/) - Local data persistence 79 | 80 | ## Contributing 81 | 82 | Contributions are welcome! Submit an issue for discussion and a PR for the code. 83 | 84 | > **Note:** Code is dirty and rushed. This was made in our free time in the short duration of the hackathon. Don't judge us too harshly :D 85 | 86 | 87 | ## Authors 88 | 89 | * **Justin Enerio** 90 | * **Edbert Estevez** 91 | * **Dale Moncayo** 92 | * **King Montayre** 93 | 94 | See also the list of [contributors](https://github.com/snap-hunt/snaphunt/graphs/contributors) who participated in this project. 95 | 96 | ## License 97 | 98 | This project is licensed under the MIT License - see the [LICENSE.md](./LICENSE.md) file for details 99 | 100 | ## Acknowledgments 101 | 102 | * Flutter PH for hosting this hackathon 103 | -------------------------------------------------------------------------------- /lib/stores/hunt_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:cloud_firestore/cloud_firestore.dart'; 5 | import 'package:firebase_ml_vision/firebase_ml_vision.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:snaphunt/data/repository.dart'; 8 | import 'package:snaphunt/model/hunt.dart'; 9 | import 'package:vibration/vibration.dart'; 10 | 11 | class HuntModel with ChangeNotifier { 12 | HuntModel({ 13 | this.objects, 14 | this.timeLimit, 15 | this.isMultiplayer = false, 16 | this.gameId, 17 | this.userId, 18 | this.timeDuration, 19 | }); 20 | 21 | final List objects; 22 | 23 | int get objectsFound => objects.where((hunt) => hunt.isFound).length; 24 | 25 | Hunt get nextNotFound { 26 | Hunt hunt; 27 | 28 | try { 29 | hunt = objects.where((hunt) => !hunt.isFound).first; 30 | } catch (e) { 31 | hunt = null; 32 | } 33 | return hunt; 34 | } 35 | 36 | final DateTime timeLimit; 37 | 38 | // multi 39 | final bool isMultiplayer; 40 | 41 | final String gameId; 42 | 43 | final String userId; 44 | 45 | final int timeDuration; 46 | 47 | bool isGameEnd = false; 48 | 49 | StreamSubscription gameStream; 50 | 51 | final ImageLabeler _imageLabeler = FirebaseVision.instance.imageLabeler( 52 | ImageLabelerOptions( 53 | confidenceThreshold: 0.65, 54 | ), 55 | ); 56 | 57 | bool isTimeUp = false; 58 | 59 | bool isHuntComplete = false; 60 | 61 | final Stopwatch duration = Stopwatch(); 62 | 63 | final repository = Repository.instance; 64 | 65 | Timer timer; 66 | 67 | @override 68 | void addListener(listener) { 69 | super.addListener(listener); 70 | init(); 71 | 72 | if (isMultiplayer) { 73 | initMultiplayer(); 74 | } 75 | } 76 | 77 | @override 78 | void dispose() { 79 | super.dispose(); 80 | 81 | timer?.cancel(); 82 | 83 | if (isMultiplayer) { 84 | disposeMultiplayer(); 85 | } 86 | } 87 | 88 | void init() { 89 | final limit = 90 | Duration(seconds: timeLimit.difference(DateTime.now()).inSeconds); 91 | duration.start(); 92 | timer = Timer(limit, () { 93 | isTimeUp = true; 94 | 95 | if (isMultiplayer) { 96 | repository.endGame(gameId); 97 | } 98 | notifyListeners(); 99 | duration.stop(); 100 | }); 101 | } 102 | 103 | void initMultiplayer() { 104 | gameStream = repository.gameSnapshot(gameId).listen(gameStatusListener); 105 | } 106 | 107 | void disposeMultiplayer() { 108 | gameStream.cancel(); 109 | } 110 | 111 | void gameStatusListener(DocumentSnapshot snapshot) async { 112 | final status = snapshot.data['status']; 113 | if (status == 'end') { 114 | isGameEnd = true; 115 | notifyListeners(); 116 | } 117 | } 118 | 119 | void _scanImage(File image) async { 120 | final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(image); 121 | final results = await _imageLabeler.processImage(visionImage); 122 | 123 | checkWords(results); 124 | } 125 | 126 | void checkWords(List scanResults) { 127 | hasMatch(scanResults).then((result) { 128 | if (result != 0) { 129 | successVibrate(); 130 | incrementScore(result); 131 | 132 | checkComplete().then((complete) { 133 | if (complete) { 134 | isHuntComplete = true; 135 | 136 | if (isMultiplayer) { 137 | repository.endGame(gameId); 138 | } 139 | } 140 | }); 141 | } else { 142 | failVibrate(); 143 | } 144 | }); 145 | notifyListeners(); 146 | } 147 | 148 | Future hasMatch(List scanResults) async { 149 | int count = 0; 150 | 151 | scanResults.forEach((results) { 152 | objects.where((hunt) => !hunt.isFound).forEach((words) { 153 | if (results.text.toLowerCase() == words.word.toLowerCase()) { 154 | count++; 155 | words.isFound = true; 156 | } 157 | }); 158 | }); 159 | 160 | return count; 161 | } 162 | 163 | void incrementScore(int increment) { 164 | if (isMultiplayer && increment != 0) { 165 | repository.updateUserScore(gameId, userId, increment); 166 | } 167 | } 168 | 169 | Future checkComplete() async { 170 | bool isComplete = false; 171 | 172 | if (objects.where((hunt) => !hunt.isFound).isEmpty) { 173 | isComplete = true; 174 | } 175 | 176 | return isComplete; 177 | } 178 | 179 | void onCameraPressed(String path) { 180 | _scanImage(File(path)); 181 | } 182 | 183 | void successVibrate() { 184 | Vibration.vibrate(pattern: [0, 250, 250, 250]); 185 | } 186 | 187 | void failVibrate() { 188 | Vibration.vibrate(pattern: [0, 250]); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/ui/multiplayer/create_room.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:hive/hive.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:snaphunt/model/game.dart'; 7 | import 'package:snaphunt/routes.dart'; 8 | import 'package:snaphunt/widgets/common/card_textfield.dart'; 9 | import 'package:snaphunt/widgets/multiplayer/create_buttons.dart'; 10 | 11 | class CreateRoom extends StatefulWidget { 12 | @override 13 | _CreateRoomState createState() => _CreateRoomState(); 14 | } 15 | 16 | class _CreateRoomState extends State { 17 | final nameController = TextEditingController(); 18 | final maxPlayersController = TextEditingController(); 19 | final itemsController = TextEditingController(); 20 | int dropdownValue = 3; 21 | 22 | @override 23 | void initState() { 24 | nameController.text = 'Snap Attack \'19'; 25 | maxPlayersController.text = '3'; 26 | itemsController.text = '8'; 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | nameController.dispose(); 33 | maxPlayersController.dispose(); 34 | itemsController.dispose(); 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final user = Provider.of(context, listen: false); 41 | final objectCount = Hive.box('words').get('words').length; 42 | 43 | return Scaffold( 44 | resizeToAvoidBottomPadding: false, 45 | appBar: AppBar( 46 | title: Text( 47 | 'New Hunt Room', 48 | style: TextStyle(color: Colors.white), 49 | ), 50 | leading: Container(), 51 | centerTitle: true, 52 | elevation: 0, 53 | ), 54 | body: ListView( 55 | physics: NeverScrollableScrollPhysics(), 56 | children: [ 57 | const SizedBox(height: 10), 58 | CardTextField( 59 | label: 'Room Name', 60 | widget: TextField( 61 | controller: nameController, 62 | keyboardType: TextInputType.text, 63 | maxLines: 1, 64 | decoration: InputDecoration( 65 | border: InputBorder.none, 66 | ), 67 | ), 68 | ), 69 | CardTextField( 70 | label: 'Max Players', 71 | widget: TextField( 72 | controller: maxPlayersController, 73 | keyboardType: TextInputType.number, 74 | maxLines: 1, 75 | decoration: InputDecoration( 76 | border: InputBorder.none, 77 | ), 78 | ), 79 | ), 80 | CardTextField( 81 | label: 'Time Limit', 82 | widget: DropdownButton( 83 | isExpanded: true, 84 | value: dropdownValue, 85 | iconSize: 24, 86 | elevation: 16, 87 | underline: Container(), 88 | onChanged: (newVal) { 89 | setState(() { 90 | dropdownValue = newVal; 91 | }); 92 | }, 93 | items: [3, 5, 8, 12, 15] 94 | .map>((int value) { 95 | return DropdownMenuItem( 96 | value: value, 97 | child: Text('$value mins'), 98 | ); 99 | }).toList(), 100 | ), 101 | ), 102 | CardTextField( 103 | label: 'No. of Items', 104 | widget: TextField( 105 | controller: itemsController, 106 | keyboardType: TextInputType.number, 107 | maxLines: 1, 108 | decoration: InputDecoration( 109 | border: InputBorder.none, 110 | ), 111 | ), 112 | ), 113 | const SizedBox(height: 20), 114 | CreateButtons( 115 | onCreate: () async { 116 | int numObjects = int.parse(itemsController.text); 117 | 118 | if (numObjects == 0) { 119 | numObjects = 1; 120 | } else if (numObjects > objectCount) { 121 | numObjects = objectCount; 122 | } 123 | 124 | Game game = Game( 125 | name: nameController.text, 126 | maxPlayers: int.parse(maxPlayersController.text), 127 | noOfItems: numObjects, 128 | timeLimit: dropdownValue, 129 | status: 'waiting', 130 | createdBy: user.uid, 131 | timeCreated: DateTime.fromMillisecondsSinceEpoch( 132 | Timestamp.now().millisecondsSinceEpoch), 133 | ); 134 | Navigator.of(context).pushReplacementNamed( 135 | Router.room, 136 | arguments: [game, true, user.uid], 137 | ); 138 | }, 139 | ) 140 | ], 141 | ), 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/data/repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:hive/hive.dart'; 4 | import 'package:snaphunt/model/game.dart'; 5 | import 'package:snaphunt/model/player.dart'; 6 | import 'package:snaphunt/model/user.dart'; 7 | import 'package:snaphunt/utils/utils.dart'; 8 | 9 | class Repository { 10 | static final Repository _singleton = Repository._(); 11 | 12 | Repository._(); 13 | 14 | factory Repository() => _singleton; 15 | 16 | static Repository get instance => _singleton; 17 | 18 | final Firestore _db = Firestore.instance; 19 | 20 | void updateUserData(FirebaseUser user) async { 21 | final DocumentReference ref = _db.collection('users').document(user.uid); 22 | 23 | return ref.setData({ 24 | 'uid': user.uid, 25 | 'email': user.email, 26 | 'photoURL': user.photoUrl, 27 | 'displayName': user.displayName, 28 | }, merge: true); 29 | } 30 | 31 | Future createRoom(Game game) async { 32 | final DocumentReference ref = 33 | await _db.collection('games').add(game.toJson()); 34 | return ref.documentID; 35 | } 36 | 37 | Future retrieveGame(String roomId) async { 38 | Game game; 39 | 40 | try { 41 | final DocumentSnapshot ref = await _db.document('games/$roomId').get(); 42 | 43 | if (ref.data != null) { 44 | game = Game.fromJson(ref.data)..id = ref.documentID; 45 | } 46 | } catch (e) { 47 | print(e); 48 | } 49 | 50 | return game; 51 | } 52 | 53 | Future joinRoom(String roomId, String userId) async { 54 | return _db 55 | .document('games/$roomId') 56 | .collection('players') 57 | .document(userId) 58 | .setData({'status': 'active', 'score': 0}); 59 | } 60 | 61 | void cancelRoom(String roomId) async { 62 | await _db.document('games/$roomId').updateData({'status': 'cancelled'}); 63 | } 64 | 65 | void leaveRoom(String roomId, String userId) async { 66 | await _db 67 | .document('games/$roomId') 68 | .collection('players') 69 | .document(userId) 70 | .delete(); 71 | } 72 | 73 | void endGame(String roomId) async { 74 | await _db.document('games/$roomId').updateData({'status': 'end'}); 75 | } 76 | 77 | void startGame(String roomId, {int numOfItems = 8}) async { 78 | await _db.document('games/$roomId').updateData({ 79 | 'status': 'in_game', 80 | 'gameStartTime': Timestamp.now(), 81 | 'words': generateWords(numOfItems) 82 | }); 83 | } 84 | 85 | Future getUserName(String uuid) async { 86 | final DocumentSnapshot ref = 87 | await _db.collection('users').document(uuid).get(); 88 | return ref['displayName']; 89 | } 90 | 91 | Future getUser(String uuid) async { 92 | final DocumentSnapshot ref = 93 | await _db.collection('users').document(uuid).get(); 94 | return User.fromJson(ref.data); 95 | } 96 | 97 | Stream gameSnapshot(String roomId) { 98 | return _db.collection('games').document(roomId).snapshots(); 99 | } 100 | 101 | Stream playersSnapshot(String gameId) { 102 | return _db 103 | .collection('games') 104 | .document(gameId) 105 | .collection('players') 106 | .snapshots(); 107 | } 108 | 109 | void kickPlayer(String gameId, String userId) async { 110 | await _db 111 | .collection('games') 112 | .document(gameId) 113 | .collection('players') 114 | .document(userId) 115 | .delete(); 116 | } 117 | 118 | Future getGamePlayerCount(String gameId) async { 119 | final players = await _db 120 | .collection('games') 121 | .document(gameId) 122 | .collection('players') 123 | .getDocuments(); 124 | return players.documents.length; 125 | } 126 | 127 | void updateLocalWords() async { 128 | final box = Hive.box('words'); 129 | 130 | final DocumentSnapshot doc = await _db.document('words/words').get(); 131 | 132 | final localVersion = box.get('version'); 133 | final onlineVersion = doc.data['version']; 134 | 135 | if (localVersion != onlineVersion) { 136 | box.put('words', doc.data['words']); 137 | box.put('version', doc.data['version']); 138 | } 139 | } 140 | 141 | Future updateUserScore(String gameId, String userId, int increment) async { 142 | final DocumentReference ref = _db.document('games/$gameId/players/$userId'); 143 | return ref.setData({ 144 | 'score': FieldValue.increment(increment), 145 | }, merge: true); 146 | } 147 | 148 | Future> getPlayers(String gameId) async { 149 | List players = []; 150 | final QuerySnapshot ref = await _db 151 | .collection('games') 152 | .document(gameId) 153 | .collection('players') 154 | .getDocuments(); 155 | 156 | for (var document in ref.documents) { 157 | final DocumentSnapshot userRef = 158 | await _db.collection('users').document(document.documentID).get(); 159 | 160 | players.add(Player.fromJson(document.data, userRef.data)); 161 | } 162 | 163 | return players; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/ui/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snaphunt/routes.dart'; 3 | import 'package:snaphunt/services/auth.dart'; 4 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 5 | 6 | import '../widgets/common/wave.dart'; 7 | 8 | class Login extends StatefulWidget { 9 | static Route route() { 10 | return MaterialPageRoute( 11 | builder: (BuildContext context) => const Login(), 12 | ); 13 | } 14 | 15 | const Login({ 16 | Key key, 17 | }) : super(key: key); 18 | 19 | @override 20 | _LoginState createState() => _LoginState(); 21 | } 22 | 23 | class LoginFancyButton extends StatelessWidget { 24 | final String text; 25 | final Color color; 26 | final Function onPressed; 27 | 28 | const LoginFancyButton({ 29 | Key key, 30 | this.text, 31 | this.color, 32 | this.onPressed, 33 | }) : super(key: key); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return FancyButton( 38 | child: SizedBox( 39 | width: 200, 40 | child: Center( 41 | child: Text( 42 | text, 43 | style: TextStyle(color: Colors.white, fontSize: 18), 44 | ), 45 | ), 46 | ), 47 | size: 70, 48 | color: color, 49 | onPressed: onPressed, 50 | ); 51 | } 52 | } 53 | 54 | class _LoginState extends State { 55 | Future _loginFuture; 56 | 57 | void _onLoginWithGooglePressed() { 58 | setState(() { 59 | _loginFuture = Auth.of(context).loginWithGoogle(); 60 | }); 61 | } 62 | 63 | void _onHowToPlay() { 64 | Navigator.of(context).pushNamed(Router.howToPlay); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Scaffold( 70 | backgroundColor: Colors.white, 71 | body: SafeArea( 72 | child: SizedBox.expand( 73 | child: Column( 74 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 75 | children: [ 76 | Image.asset('assets/top.png', scale: 1), 77 | Image.asset('assets/main.png', height: 185), 78 | FutureBuilder( 79 | future: _loginFuture, 80 | builder: (BuildContext context, AsyncSnapshot snapshot) { 81 | if (snapshot.connectionState == ConnectionState.waiting) { 82 | return const Center( 83 | child: CircularProgressIndicator(), 84 | ); 85 | } 86 | return Padding( 87 | padding: const EdgeInsets.symmetric(horizontal: 0.0), 88 | child: Column( 89 | children: [ 90 | Container( 91 | child: LoginFancyButton( 92 | text: 'Login with Google', 93 | color: Color(0xffFF951A), 94 | onPressed: _onLoginWithGooglePressed, 95 | ), 96 | ), 97 | Container( 98 | margin: const EdgeInsets.only(top: 18.0), 99 | child: LoginFancyButton( 100 | text: 'How to Play', 101 | color: Colors.grey, 102 | onPressed: _onHowToPlay, 103 | ), 104 | ), 105 | if (snapshot.hasError) 106 | Container( 107 | width: double.infinity, 108 | margin: const EdgeInsets.symmetric(vertical: 16.0), 109 | padding: const EdgeInsets.all(12.0), 110 | decoration: BoxDecoration( 111 | border: 112 | Border.all(color: Theme.of(context).errorColor), 113 | borderRadius: 114 | const BorderRadius.all(Radius.circular(2.0)), 115 | color: 116 | Theme.of(context).errorColor.withOpacity(0.6), 117 | ), 118 | child: Text( 119 | snapshot.error.toString(), 120 | style: TextStyle( 121 | color: Colors.white, 122 | ), 123 | textAlign: TextAlign.center, 124 | ), 125 | ), 126 | ], 127 | ), 128 | ); 129 | }, 130 | ), 131 | Container( 132 | alignment: Alignment.bottomCenter, 133 | child: Column(children: [ 134 | Container( 135 | margin: EdgeInsets.only(bottom: 12), 136 | child: Text("Flutter PH Hackathon Entry", 137 | style: TextStyle( 138 | color: Colors.grey, 139 | fontSize: 16, 140 | fontWeight: FontWeight.bold)), 141 | ), 142 | CustomWaveWidget(), 143 | ])), 144 | ], 145 | )), 146 | ), 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/widgets/common/fancy_button.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | 4 | // source: https://gist.github.com/mkiisoft/0d5c013114a2101d69f5d4a780d936cd 5 | class FancyButton extends StatefulWidget { 6 | const FancyButton({ 7 | Key key, 8 | @required this.child, 9 | @required this.size, 10 | @required this.color, 11 | this.duration = const Duration(milliseconds: 160), 12 | this.onPressed, 13 | }) : super(key: key); 14 | 15 | final Widget child; 16 | final Color color; 17 | final Duration duration; 18 | final VoidCallback onPressed; 19 | 20 | final double size; 21 | 22 | @override 23 | _FancyButtonState createState() => _FancyButtonState(); 24 | } 25 | 26 | class _FancyButtonState extends State with TickerProviderStateMixin { 27 | AnimationController _animationController; 28 | Animation _pressedAnimation; 29 | 30 | TickerFuture _downTicker; 31 | 32 | double get buttonDepth => widget.size * 0.2; 33 | 34 | void _setupAnimation() { 35 | _animationController?.stop(); 36 | final oldControllerValue = _animationController?.value ?? 0.0; 37 | _animationController?.dispose(); 38 | _animationController = AnimationController( 39 | duration: Duration(microseconds: widget.duration.inMicroseconds ~/ 2), 40 | vsync: this, 41 | value: oldControllerValue, 42 | ); 43 | _pressedAnimation = Tween(begin: -buttonDepth, end: 0.0).animate( 44 | CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), 45 | ); 46 | } 47 | 48 | @override 49 | void didUpdateWidget(FancyButton oldWidget) { 50 | super.didUpdateWidget(oldWidget); 51 | if (oldWidget.duration != widget.duration) { 52 | _setupAnimation(); 53 | } 54 | } 55 | 56 | @override 57 | void didChangeDependencies() { 58 | super.didChangeDependencies(); 59 | _setupAnimation(); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | _animationController.dispose(); 65 | super.dispose(); 66 | } 67 | 68 | void _onTapDown(_) { 69 | if (widget.onPressed != null) { 70 | _downTicker = _animationController.animateTo(1.0); 71 | } 72 | } 73 | 74 | void _onTapUp(_) { 75 | if (widget.onPressed != null) { 76 | _downTicker.whenComplete(() { 77 | _animationController.animateTo(0.0); 78 | widget.onPressed?.call(); 79 | }); 80 | } 81 | } 82 | 83 | void _onTapCancel() { 84 | if (widget.onPressed != null) { 85 | _animationController.reset(); 86 | } 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | final vertPadding = widget.size * 0.25; 92 | final horzPadding = widget.size * 0.50; 93 | final radius = BorderRadius.circular(horzPadding * 0.5); 94 | 95 | return Container( 96 | padding: widget.onPressed != null ? EdgeInsets.only(bottom: 2, left: 0.5, right: 0.5) : null, 97 | decoration: BoxDecoration( 98 | color: Colors.black87, 99 | borderRadius: radius, 100 | ), 101 | child: GestureDetector( 102 | onTapDown: _onTapDown, 103 | onTapUp: _onTapUp, 104 | onTapCancel: _onTapCancel, 105 | child: IntrinsicWidth( 106 | child: IntrinsicHeight( 107 | child: Stack( 108 | children: [ 109 | Container( 110 | decoration: BoxDecoration( 111 | color: _hslRelativeColor(s: -0.20, l: -0.20), 112 | borderRadius: radius, 113 | ), 114 | ), 115 | AnimatedBuilder( 116 | animation: _pressedAnimation, 117 | builder: (BuildContext context, Widget child) { 118 | return Transform.translate( 119 | offset: Offset(0.0, _pressedAnimation.value), 120 | child: child, 121 | ); 122 | }, 123 | child: Stack( 124 | overflow: Overflow.visible, 125 | children: [ 126 | ClipRRect( 127 | borderRadius: radius, 128 | child: Stack( 129 | children: [ 130 | DecoratedBox( 131 | decoration: BoxDecoration( 132 | color: _hslRelativeColor(l: 0.06), 133 | borderRadius: radius, 134 | ), 135 | child: SizedBox.expand(), 136 | ), 137 | Transform.translate( 138 | offset: Offset(0.0, vertPadding * 2), 139 | child: DecoratedBox( 140 | decoration: BoxDecoration( 141 | color: _hslRelativeColor(), 142 | borderRadius: radius, 143 | ), 144 | child: SizedBox.expand(), 145 | ), 146 | ), 147 | ], 148 | ), 149 | ), 150 | Padding( 151 | padding: EdgeInsets.symmetric( 152 | vertical: vertPadding, 153 | horizontal: horzPadding, 154 | ), 155 | child: widget.child, 156 | ) 157 | ], 158 | ), 159 | ), 160 | ], 161 | ), 162 | ), 163 | ), 164 | ), 165 | ); 166 | } 167 | 168 | Color _hslRelativeColor({double h = 0.0, s = 0.0, l = 0.0}) { 169 | final hslColor = HSLColor.fromColor(widget.color); 170 | h = (hslColor.hue + h).clamp(0.0, 360.0); 171 | s = (hslColor.saturation + s).clamp(0.0, 1.0); 172 | l = (hslColor.lightness + l).clamp(0.0, 1.0); 173 | return HSLColor.fromAHSL(hslColor.alpha, h, s, l).toColor(); 174 | } 175 | } -------------------------------------------------------------------------------- /lib/widgets/multiplayer/join_room_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; 3 | import 'package:snaphunt/constants/app_theme.dart'; 4 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 5 | 6 | import 'package:flutter/services.dart'; 7 | 8 | class JoinRoom extends StatelessWidget { 9 | Future scanQR() async { 10 | String barcodeScanRes; 11 | 12 | try { 13 | barcodeScanRes = await FlutterBarcodeScanner.scanBarcode( 14 | "#ff6666", "Cancel", true, ScanMode.QR); 15 | } on PlatformException { 16 | barcodeScanRes = null; 17 | } catch (ex) { 18 | barcodeScanRes = null; 19 | } 20 | return barcodeScanRes; 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final controller = TextEditingController(); 26 | 27 | return AlertDialog( 28 | shape: RoundedRectangleBorder( 29 | borderRadius: BorderRadius.circular(18.0), 30 | ), 31 | elevation: 5, 32 | title: Center( 33 | child: Text( 34 | 'Enter Room Code', 35 | style: TextStyle( 36 | fontSize: 28, 37 | fontWeight: FontWeight.bold, 38 | ), 39 | ), 40 | ), 41 | content: SingleChildScrollView( 42 | scrollDirection: Axis.vertical, 43 | child: Column( 44 | mainAxisSize: MainAxisSize.min, 45 | crossAxisAlignment: CrossAxisAlignment.center, 46 | children: [ 47 | DialogCardTextField(controller: controller), 48 | const SizedBox(height: 15), 49 | DialogFancyButton( 50 | text: 'Join Room', 51 | color: Colors.deepOrangeAccent, 52 | onPressed: () { 53 | final code = controller.text; 54 | if (code != null && code.isNotEmpty) { 55 | Navigator.of(context).pop(code); 56 | } 57 | }, 58 | ), 59 | const SizedBox(height: 10), 60 | const DividerDialog(), 61 | const SizedBox(height: 20), 62 | DialogFancyButton( 63 | text: 'Scan QR Code', 64 | color: Colors.orange, 65 | onPressed: () async { 66 | final code = await scanQR(); 67 | if (code != null && code != '-1') { 68 | Navigator.of(context).pop(code); 69 | } 70 | }, 71 | ), 72 | SizedBox(height: 15), 73 | DialogFancyButton( 74 | text: 'Close', 75 | color: Colors.blueGrey, 76 | onPressed: () { 77 | Navigator.of(context).pop(); 78 | }, 79 | ), 80 | ], 81 | ), 82 | ), 83 | ); 84 | } 85 | } 86 | 87 | class DialogFancyButton extends StatelessWidget { 88 | final String text; 89 | final Color color; 90 | final Function onPressed; 91 | 92 | const DialogFancyButton({ 93 | Key key, 94 | this.text, 95 | this.color, 96 | this.onPressed, 97 | }) : super(key: key); 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | return FancyButton( 102 | child: SizedBox( 103 | width: 150, 104 | child: Center( 105 | child: Text( 106 | text, 107 | style: fancy_button_style, 108 | ), 109 | ), 110 | ), 111 | size: 60, 112 | color: color, 113 | onPressed: onPressed, 114 | ); 115 | } 116 | } 117 | 118 | class DialogCardTextField extends StatelessWidget { 119 | final TextEditingController controller; 120 | 121 | const DialogCardTextField({Key key, this.controller}) : super(key: key); 122 | @override 123 | Widget build(BuildContext context) { 124 | return Container( 125 | margin: const EdgeInsets.symmetric( 126 | horizontal: 8.0, 127 | vertical: 2.0, 128 | ), 129 | height: 65, 130 | child: Card( 131 | shape: RoundedRectangleBorder( 132 | borderRadius: BorderRadius.circular(8.0), 133 | ), 134 | elevation: 3, 135 | child: Container( 136 | width: 300, 137 | margin: EdgeInsets.all(8.0), 138 | child: Row( 139 | mainAxisSize: MainAxisSize.max, 140 | crossAxisAlignment: CrossAxisAlignment.baseline, 141 | textBaseline: TextBaseline.alphabetic, 142 | children: [ 143 | Expanded( 144 | child: Container( 145 | child: TextField( 146 | textAlign: TextAlign.center, 147 | controller: controller, 148 | keyboardType: TextInputType.text, 149 | maxLines: 1, 150 | decoration: InputDecoration( 151 | border: InputBorder.none, 152 | ), 153 | ), 154 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 155 | ), 156 | ) 157 | ], 158 | ), 159 | ), 160 | ), 161 | ); 162 | } 163 | } 164 | 165 | class DividerDialog extends StatelessWidget { 166 | const DividerDialog({Key key}) : super(key: key); 167 | 168 | @override 169 | Widget build(BuildContext context) { 170 | return Row( 171 | mainAxisSize: MainAxisSize.max, 172 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 173 | crossAxisAlignment: CrossAxisAlignment.center, 174 | children: [ 175 | Expanded( 176 | child: Container( 177 | height: 1, 178 | color: Colors.blueGrey, 179 | ), 180 | ), 181 | SizedBox( 182 | width: 60, 183 | child: Center( 184 | child: Text( 185 | 'or', 186 | style: TextStyle(fontSize: 18), 187 | ), 188 | ), 189 | ), 190 | Expanded( 191 | child: Container( 192 | height: 1, 193 | color: Colors.blueGrey, 194 | ), 195 | ), 196 | ], 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/widgets/common/camera.dart: -------------------------------------------------------------------------------- 1 | import 'package:camera/camera.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:path/path.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:snaphunt/main.dart'; 7 | import 'package:snaphunt/stores/hunt_model.dart'; 8 | 9 | class CameraScreen extends StatefulWidget { 10 | const CameraScreen({Key key}) : super(key: key); 11 | 12 | @override 13 | _CameraScreenState createState() => _CameraScreenState(); 14 | } 15 | 16 | class _CameraScreenState extends State 17 | with WidgetsBindingObserver { 18 | CameraController controller; 19 | String imagePath; 20 | int selectedCameraIdx = 0; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | onNewCameraSelected(cameras[selectedCameraIdx]); 26 | WidgetsBinding.instance.addObserver(this); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | WidgetsBinding.instance.removeObserver(this); 32 | super.dispose(); 33 | } 34 | 35 | @override 36 | void didChangeAppLifecycleState(AppLifecycleState state) { 37 | if (controller == null || !controller.value.isInitialized) { 38 | return; 39 | } 40 | if (state == AppLifecycleState.inactive) { 41 | controller?.dispose(); 42 | } else if (state == AppLifecycleState.resumed) { 43 | if (controller != null) { 44 | onNewCameraSelected(cameras[selectedCameraIdx]); 45 | } 46 | } 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return Scaffold( 52 | body: Stack( 53 | children: [ 54 | Container( 55 | color: Colors.black, 56 | width: double.infinity, 57 | height: double.infinity, 58 | child: controller != null && controller.value.isInitialized 59 | ? AspectRatio( 60 | aspectRatio: controller.value.aspectRatio, 61 | child: CameraPreview(controller), 62 | ) 63 | : Center(child: CircularProgressIndicator()), 64 | ), 65 | Align( 66 | alignment: Alignment.bottomCenter, 67 | child: CameraRow( 68 | controller: controller, 69 | onCameraSwitch: _onSwitchCamera, 70 | ), 71 | ), 72 | ], 73 | ), 74 | ); 75 | } 76 | 77 | void _onSwitchCamera() { 78 | selectedCameraIdx = 79 | selectedCameraIdx < cameras.length - 1 ? selectedCameraIdx + 1 : 0; 80 | onNewCameraSelected(cameras[selectedCameraIdx]); 81 | } 82 | 83 | void onNewCameraSelected(CameraDescription cameraDescription) async { 84 | if (controller != null) { 85 | await controller.dispose(); 86 | } 87 | 88 | controller = CameraController( 89 | cameraDescription, 90 | ResolutionPreset.high, 91 | ); 92 | 93 | controller.addListener(() { 94 | if (mounted) setState(() {}); 95 | }); 96 | 97 | try { 98 | await controller.initialize(); 99 | } on CameraException catch (e) { 100 | print(e.description); 101 | } 102 | 103 | if (mounted) { 104 | setState(() {}); 105 | } 106 | } 107 | } 108 | 109 | class CameraRow extends StatelessWidget { 110 | final CameraController controller; 111 | final Function onCameraSwitch; 112 | 113 | const CameraRow({ 114 | Key key, 115 | this.controller, 116 | this.onCameraSwitch, 117 | }) : super(key: key); 118 | 119 | @override 120 | Widget build(BuildContext context) { 121 | return Container( 122 | color: Colors.black.withOpacity(0.4), 123 | height: 100, 124 | child: Stack( 125 | children: [ 126 | Align( 127 | alignment: Alignment.center, 128 | child: CameraButton(controller: controller), 129 | ), 130 | Positioned( 131 | child: CameraSwapButton( 132 | onPressed: onCameraSwitch, 133 | ), 134 | height: 100, 135 | left: MediaQuery.of(context).size.width * 0.75, 136 | ) 137 | ], 138 | ), 139 | ); 140 | } 141 | } 142 | 143 | class CameraSwapButton extends StatelessWidget { 144 | final Function onPressed; 145 | 146 | const CameraSwapButton({Key key, this.onPressed}) : super(key: key); 147 | 148 | @override 149 | Widget build(BuildContext context) { 150 | return Container( 151 | decoration: BoxDecoration( 152 | shape: BoxShape.circle, 153 | border: Border.all( 154 | width: 1, 155 | color: Colors.white, 156 | ), 157 | ), 158 | child: IconButton( 159 | icon: Icon( 160 | Icons.switch_camera, 161 | color: Colors.white, 162 | ), 163 | onPressed: onPressed, 164 | ), 165 | ); 166 | } 167 | } 168 | 169 | class CameraButton extends StatelessWidget { 170 | final CameraController controller; 171 | 172 | const CameraButton({ 173 | Key key, 174 | this.controller, 175 | }) : super(key: key); 176 | 177 | Future onCapturePressed() async { 178 | String _path; 179 | 180 | try { 181 | final path = join( 182 | (await getTemporaryDirectory()).path, 183 | '${DateTime.now()}.png', 184 | ); 185 | await controller.takePicture(path); 186 | _path = path; 187 | } catch (e) { 188 | print(e); 189 | } 190 | 191 | return _path; 192 | } 193 | 194 | @override 195 | Widget build(BuildContext context) { 196 | final model = Provider.of(context, listen: false); 197 | return InkWell( 198 | onTap: () async { 199 | model.onCameraPressed(await onCapturePressed()); 200 | }, 201 | child: Container( 202 | padding: const EdgeInsets.all(6), 203 | height: 70, 204 | width: 70, 205 | decoration: BoxDecoration( 206 | shape: BoxShape.circle, 207 | color: Colors.deepOrange, 208 | ), 209 | child: Container( 210 | decoration: BoxDecoration( 211 | shape: BoxShape.circle, 212 | color: Colors.white, 213 | ), 214 | ), 215 | ), 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /lib/ui/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:snaphunt/routes.dart'; 5 | import 'package:snaphunt/services/auth.dart'; 6 | import 'package:snaphunt/services/connectivity.dart'; 7 | import 'package:snaphunt/utils/utils.dart'; 8 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 9 | import 'package:snaphunt/widgets/common/wave.dart'; 10 | 11 | class Home extends StatefulWidget { 12 | static Route route() { 13 | return MaterialPageRoute( 14 | builder: (BuildContext context) => const Home(), 15 | ); 16 | } 17 | 18 | const Home({ 19 | Key key, 20 | }) : super(key: key); 21 | 22 | @override 23 | _HomeState createState() => _HomeState(); 24 | } 25 | 26 | class HomeFancyButton extends StatelessWidget { 27 | final String text; 28 | final Color color; 29 | final Function onPressed; 30 | final Widget child; 31 | final double width; 32 | 33 | const HomeFancyButton( 34 | {Key key, 35 | this.text, 36 | this.color, 37 | this.onPressed, 38 | this.child, 39 | this.width = 220}) 40 | : super(key: key); 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return FancyButton( 45 | child: SizedBox( 46 | width: width, 47 | child: child, 48 | ), 49 | size: 70, 50 | color: color, 51 | onPressed: onPressed, 52 | ); 53 | } 54 | } 55 | 56 | class _HomeState extends State { 57 | @override 58 | Widget build(BuildContext context) { 59 | var connectionStatus = Provider.of(context); 60 | 61 | return Scaffold( 62 | backgroundColor: Colors.white, 63 | body: SafeArea( 64 | child: SizedBox.expand( 65 | child: Column( 66 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 67 | crossAxisAlignment: CrossAxisAlignment.center, 68 | mainAxisSize: MainAxisSize.max, 69 | children: [ 70 | Container( 71 | padding: EdgeInsets.only(left: 30, right: 30, top: 50), 72 | color: Colors.white, 73 | child: Image.asset('assets/main.png', height: 185), 74 | ), 75 | Column( 76 | mainAxisAlignment: MainAxisAlignment.end, 77 | children: [ 78 | const UserInfo(), 79 | const SizedBox(height: 32.0), 80 | HomeFancyButton( 81 | child: Row( 82 | mainAxisAlignment: MainAxisAlignment.center, 83 | children: [ 84 | Text( 85 | "Singleplayer", 86 | style: TextStyle(color: Colors.white, fontSize: 18), 87 | ), 88 | ]), 89 | color: Colors.orange, 90 | onPressed: () { 91 | Navigator.of(context) 92 | .pushNamed(Router.singlePlayerSettings); 93 | }, 94 | ), 95 | const SizedBox(height: 18.0), 96 | HomeFancyButton( 97 | child: Row( 98 | mainAxisAlignment: MainAxisAlignment.center, 99 | children: [ 100 | Text( 101 | "Multiplayer", 102 | style: TextStyle(color: Colors.white, fontSize: 18), 103 | ), 104 | ]), 105 | color: connectionStatus == ConnectivityStatus.Offline 106 | ? Colors.grey 107 | : Colors.blue, 108 | onPressed: connectionStatus == ConnectivityStatus.Offline 109 | ? () { 110 | showAlertDialog( 111 | context: context, 112 | title: 'You are offline!', 113 | body: 114 | 'Internet connection is needed to play online', 115 | ); 116 | } 117 | : () { 118 | Navigator.of(context).pushNamed(Router.lobby); 119 | }, 120 | ), 121 | const SizedBox(height: 18.0), 122 | Container( 123 | child: HomeFancyButton( 124 | width: 125, 125 | child: Row( 126 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 127 | children: [ 128 | Padding( 129 | padding: const EdgeInsets.all(0), 130 | child: Icon( 131 | Icons.power_settings_new, 132 | color: Colors.white, 133 | size: 18, 134 | ), 135 | ), 136 | Container( 137 | margin: EdgeInsets.only(left: 2), 138 | child: Text( 139 | "Logout", 140 | style: TextStyle( 141 | color: Colors.white, fontSize: 18), 142 | )) 143 | ], 144 | ), 145 | onPressed: () { 146 | Auth.of(context).logout(); 147 | }, 148 | color: Colors.red, 149 | ), 150 | ), 151 | ], 152 | ), 153 | CustomWaveWidget() 154 | ], 155 | ), 156 | ), 157 | ), 158 | ); 159 | } 160 | } 161 | 162 | class UserInfo extends StatelessWidget { 163 | const UserInfo({ 164 | Key key, 165 | }) : super(key: key); 166 | 167 | @override 168 | Widget build(BuildContext context) { 169 | final user = Provider.of(context, listen: false); 170 | 171 | return Container( 172 | child: Row( 173 | mainAxisAlignment: MainAxisAlignment.center, 174 | crossAxisAlignment: CrossAxisAlignment.center, 175 | mainAxisSize: MainAxisSize.max, 176 | children: [ 177 | UserAvatar( 178 | photoUrl: user.photoUrl, 179 | ), 180 | SizedBox(width: 15), 181 | Text( 182 | user.displayName, 183 | style: TextStyle( 184 | fontSize: 28, 185 | fontWeight: FontWeight.w400, 186 | ), 187 | maxLines: 2, 188 | ) 189 | ], 190 | ), 191 | ); 192 | } 193 | } 194 | 195 | /// Displays the user's image 196 | class UserAvatar extends StatelessWidget { 197 | final String photoUrl; 198 | final double height; 199 | final Color borderColor; 200 | 201 | const UserAvatar({ 202 | Key key, 203 | this.photoUrl, 204 | this.height = 70.0, 205 | this.borderColor = Colors.orange, 206 | }) : super(key: key); 207 | 208 | @override 209 | Widget build(BuildContext context) { 210 | return Column( 211 | children: [ 212 | Material( 213 | elevation: 2.0, 214 | shape: CircleBorder( 215 | side: BorderSide(color: borderColor, width: 3.0), 216 | ), 217 | color: Colors.black, 218 | clipBehavior: Clip.antiAlias, 219 | child: SizedBox( 220 | width: height, 221 | height: height, 222 | child: photoUrl != null 223 | ? Image.network(photoUrl) 224 | : Icon( 225 | Icons.person, 226 | color: Colors.white, 227 | size: 72.0, 228 | ), 229 | ), 230 | ), 231 | ], 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /lib/ui/multiplayer/lobby.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:snaphunt/data/repository.dart'; 7 | import 'package:snaphunt/model/game.dart'; 8 | import 'package:snaphunt/routes.dart'; 9 | import 'package:snaphunt/utils/utils.dart'; 10 | import 'package:snaphunt/widgets/multiplayer/join_room_dialog.dart'; 11 | import 'package:snaphunt/widgets/multiplayer/lobby_buttons.dart'; 12 | 13 | class Lobby extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: Text( 19 | 'SNAPHUNT LOBBY', 20 | style: TextStyle(color: Colors.white), 21 | ), 22 | automaticallyImplyLeading: false, 23 | centerTitle: true, 24 | elevation: 0, 25 | ), 26 | body: SafeArea( 27 | child: Container( 28 | padding: const EdgeInsets.symmetric(vertical: 8.0), 29 | child: Column( 30 | children: [ 31 | Expanded( 32 | child: LobbyList(), 33 | ), 34 | Divider( 35 | thickness: 1.5, 36 | ), 37 | LobbyButtons( 38 | onCreateRoom: () { 39 | Navigator.of(context).pushNamed(Router.create); 40 | }, 41 | onJoinRoom: () async { 42 | String roomCode = await showDialog( 43 | context: context, 44 | barrierDismissible: false, 45 | builder: (BuildContext context) => JoinRoom(), 46 | ); 47 | 48 | if (roomCode != null && roomCode.isNotEmpty) { 49 | final game = 50 | await Repository.instance.retrieveGame(roomCode); 51 | 52 | if (game == null) { 53 | showAlertDialog( 54 | context: context, 55 | title: 'Invalid Code', 56 | body: 'Invalid code. Game does not exist!', 57 | ); 58 | } else { 59 | if (game.status != 'waiting') { 60 | showAlertDialog( 61 | context: context, 62 | title: 'Game not available!', 63 | body: 'Game has already started or ended!', 64 | ); 65 | } else { 66 | final user = 67 | Provider.of(context, listen: false); 68 | 69 | Navigator.of(context).pushNamed(Router.room, 70 | arguments: [game, false, user.uid]); 71 | } 72 | } 73 | } 74 | }, 75 | ), 76 | ], 77 | ), 78 | ), 79 | ), 80 | ); 81 | } 82 | } 83 | 84 | class LobbyList extends StatelessWidget { 85 | @override 86 | Widget build(BuildContext context) { 87 | return Container( 88 | child: StreamBuilder( 89 | stream: Firestore.instance 90 | .collection('games') 91 | .where('status', isEqualTo: 'waiting') 92 | .snapshots(), 93 | builder: (context, snapshot) { 94 | if (!snapshot.hasData) 95 | return Center( 96 | child: CircularProgressIndicator(), 97 | ); 98 | 99 | if (snapshot.data.documents.isEmpty) { 100 | return Container( 101 | child: Center( 102 | child: Text( 103 | 'No rooms available', 104 | style: TextStyle( 105 | fontSize: 24, 106 | fontWeight: FontWeight.bold, 107 | color: Colors.grey, 108 | ), 109 | ), 110 | ), 111 | ); 112 | } 113 | return AnimationLimiter( 114 | child: ListView.builder( 115 | itemCount: snapshot.data.documents.length, 116 | itemBuilder: (context, index) { 117 | final game = Game.fromJson(snapshot.data.documents[index].data); 118 | game.id = snapshot.data.documents[index].documentID; 119 | 120 | return AnimationConfiguration.staggeredList( 121 | position: index, 122 | duration: const Duration(milliseconds: 650), 123 | child: SlideAnimation( 124 | verticalOffset: 50.0, 125 | child: FadeInAnimation( 126 | child: LobbyListTile( 127 | game: game, 128 | onRoomClick: () async { 129 | final user = 130 | Provider.of(context, listen: false); 131 | Navigator.of(context).pushNamed(Router.room, 132 | arguments: [game, false, user.uid]); 133 | }, 134 | ), 135 | ), 136 | ), 137 | ); 138 | }, 139 | ), 140 | ); 141 | }, 142 | ), 143 | ); 144 | } 145 | } 146 | 147 | class LobbyListTile extends StatefulWidget { 148 | final Game game; 149 | final Function onRoomClick; 150 | 151 | const LobbyListTile({ 152 | Key key, 153 | this.onRoomClick, 154 | this.game, 155 | }) : super(key: key); 156 | 157 | @override 158 | _LobbyListTileState createState() => _LobbyListTileState(); 159 | } 160 | 161 | class _LobbyListTileState extends State { 162 | String _createdBy = ''; 163 | bool _isRoomFull = false; 164 | 165 | @override 166 | void initState() { 167 | getName(); 168 | super.initState(); 169 | } 170 | 171 | void getName() async { 172 | final name = await Repository.instance.getUserName(widget.game.createdBy); 173 | 174 | if (mounted) { 175 | setState(() { 176 | _createdBy = name; 177 | }); 178 | } 179 | } 180 | 181 | @override 182 | Widget build(BuildContext context) { 183 | return Container( 184 | margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 185 | child: Card( 186 | shape: RoundedRectangleBorder( 187 | borderRadius: BorderRadius.circular(8.0), 188 | ), 189 | elevation: 4, 190 | child: Container( 191 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 192 | child: ListTile( 193 | title: Text( 194 | widget.game.name, 195 | style: TextStyle( 196 | fontSize: 18, 197 | fontWeight: FontWeight.bold, 198 | ), 199 | ), 200 | subtitle: Text(_createdBy), 201 | trailing: Column( 202 | mainAxisSize: MainAxisSize.max, 203 | mainAxisAlignment: MainAxisAlignment.center, 204 | children: [ 205 | Text('${widget.game.timeLimit} mins'), 206 | const SizedBox(height: 8.0), 207 | StreamBuilder( 208 | stream: Firestore.instance 209 | .collection('games') 210 | .document(widget.game.id) 211 | .collection('players') 212 | .snapshots(), 213 | builder: (context, snapshot) { 214 | if (snapshot.data == null) { 215 | return Text('0/${widget.game.maxPlayers}'); 216 | } 217 | 218 | final players = snapshot.data.documents.length; 219 | final isRoomFull = players == widget.game.maxPlayers; 220 | 221 | return Text( 222 | '$players/${widget.game.maxPlayers}', 223 | style: TextStyle(color: isRoomFull ? Colors.red : null), 224 | ); 225 | }, 226 | ), 227 | ], 228 | ), 229 | onTap: _isRoomFull ? null : widget.onRoomClick, 230 | ), 231 | ), 232 | ), 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.10" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.3.0" 25 | auto_size_text: 26 | dependency: "direct main" 27 | description: 28 | name: auto_size_text 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.1.0" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.0.5" 39 | camera: 40 | dependency: "direct main" 41 | description: 42 | name: camera 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.5.6+3" 46 | charcode: 47 | dependency: transitive 48 | description: 49 | name: charcode 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.1.2" 53 | cloud_firestore: 54 | dependency: "direct main" 55 | description: 56 | name: cloud_firestore 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "0.12.9+6" 60 | collection: 61 | dependency: transitive 62 | description: 63 | name: collection 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.14.11" 67 | color: 68 | dependency: transitive 69 | description: 70 | name: color 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "2.1.1" 74 | connectivity: 75 | dependency: "direct main" 76 | description: 77 | name: connectivity 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "0.4.5+2" 81 | convert: 82 | dependency: transitive 83 | description: 84 | name: convert 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "2.1.1" 88 | crypto: 89 | dependency: transitive 90 | description: 91 | name: crypto 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "2.1.3" 95 | cupertino_icons: 96 | dependency: "direct main" 97 | description: 98 | name: cupertino_icons 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "0.1.2" 102 | esys_flutter_share: 103 | dependency: "direct main" 104 | description: 105 | name: esys_flutter_share 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.0.2" 109 | expandable: 110 | dependency: "direct main" 111 | description: 112 | name: expandable 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "3.0.1" 116 | firebase_auth: 117 | dependency: "direct main" 118 | description: 119 | name: firebase_auth 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "0.14.0+5" 123 | firebase_core: 124 | dependency: "direct main" 125 | description: 126 | name: firebase_core 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "0.4.0+9" 130 | firebase_ml_vision: 131 | dependency: "direct main" 132 | description: 133 | name: firebase_ml_vision 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "0.9.2+2" 137 | flutter: 138 | dependency: "direct main" 139 | description: flutter 140 | source: sdk 141 | version: "0.0.0" 142 | flutter_barcode_scanner: 143 | dependency: "direct main" 144 | description: 145 | name: flutter_barcode_scanner 146 | url: "https://pub.dartlang.org" 147 | source: hosted 148 | version: "0.1.7" 149 | flutter_native_splash: 150 | dependency: "direct dev" 151 | description: 152 | name: flutter_native_splash 153 | url: "https://pub.dartlang.org" 154 | source: hosted 155 | version: "0.1.9" 156 | flutter_staggered_animations: 157 | dependency: "direct main" 158 | description: 159 | name: flutter_staggered_animations 160 | url: "https://pub.dartlang.org" 161 | source: hosted 162 | version: "0.1.2" 163 | flutter_test: 164 | dependency: "direct dev" 165 | description: flutter 166 | source: sdk 167 | version: "0.0.0" 168 | google_sign_in: 169 | dependency: "direct main" 170 | description: 171 | name: google_sign_in 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "4.0.11" 175 | hive: 176 | dependency: "direct main" 177 | description: 178 | name: hive 179 | url: "https://pub.dartlang.org" 180 | source: hosted 181 | version: "1.1.1" 182 | image: 183 | dependency: transitive 184 | description: 185 | name: image 186 | url: "https://pub.dartlang.org" 187 | source: hosted 188 | version: "2.1.8" 189 | intro_views_flutter: 190 | dependency: "direct main" 191 | description: 192 | name: intro_views_flutter 193 | url: "https://pub.dartlang.org" 194 | source: hosted 195 | version: "2.8.0" 196 | matcher: 197 | dependency: transitive 198 | description: 199 | name: matcher 200 | url: "https://pub.dartlang.org" 201 | source: hosted 202 | version: "0.12.5" 203 | meta: 204 | dependency: transitive 205 | description: 206 | name: meta 207 | url: "https://pub.dartlang.org" 208 | source: hosted 209 | version: "1.1.7" 210 | path: 211 | dependency: transitive 212 | description: 213 | name: path 214 | url: "https://pub.dartlang.org" 215 | source: hosted 216 | version: "1.6.4" 217 | path_provider: 218 | dependency: "direct main" 219 | description: 220 | name: path_provider 221 | url: "https://pub.dartlang.org" 222 | source: hosted 223 | version: "1.4.0" 224 | pedantic: 225 | dependency: transitive 226 | description: 227 | name: pedantic 228 | url: "https://pub.dartlang.org" 229 | source: hosted 230 | version: "1.8.0+1" 231 | petitparser: 232 | dependency: transitive 233 | description: 234 | name: petitparser 235 | url: "https://pub.dartlang.org" 236 | source: hosted 237 | version: "2.4.0" 238 | platform: 239 | dependency: transitive 240 | description: 241 | name: platform 242 | url: "https://pub.dartlang.org" 243 | source: hosted 244 | version: "2.2.1" 245 | pointycastle: 246 | dependency: transitive 247 | description: 248 | name: pointycastle 249 | url: "https://pub.dartlang.org" 250 | source: hosted 251 | version: "1.0.1" 252 | provider: 253 | dependency: "direct main" 254 | description: 255 | name: provider 256 | url: "https://pub.dartlang.org" 257 | source: hosted 258 | version: "3.1.0+1" 259 | qr: 260 | dependency: transitive 261 | description: 262 | name: qr 263 | url: "https://pub.dartlang.org" 264 | source: hosted 265 | version: "1.2.0" 266 | qr_flutter: 267 | dependency: "direct main" 268 | description: 269 | name: qr_flutter 270 | url: "https://pub.dartlang.org" 271 | source: hosted 272 | version: "3.0.1" 273 | quiver: 274 | dependency: transitive 275 | description: 276 | name: quiver 277 | url: "https://pub.dartlang.org" 278 | source: hosted 279 | version: "2.0.5" 280 | screenshot: 281 | dependency: "direct main" 282 | description: 283 | name: screenshot 284 | url: "https://pub.dartlang.org" 285 | source: hosted 286 | version: "0.1.1" 287 | sky_engine: 288 | dependency: transitive 289 | description: flutter 290 | source: sdk 291 | version: "0.0.99" 292 | source_span: 293 | dependency: transitive 294 | description: 295 | name: source_span 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "1.5.5" 299 | stack_trace: 300 | dependency: transitive 301 | description: 302 | name: stack_trace 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "1.9.3" 306 | stream_channel: 307 | dependency: transitive 308 | description: 309 | name: stream_channel 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "2.0.0" 313 | string_scanner: 314 | dependency: transitive 315 | description: 316 | name: string_scanner 317 | url: "https://pub.dartlang.org" 318 | source: hosted 319 | version: "1.0.5" 320 | term_glyph: 321 | dependency: transitive 322 | description: 323 | name: term_glyph 324 | url: "https://pub.dartlang.org" 325 | source: hosted 326 | version: "1.1.0" 327 | test_api: 328 | dependency: transitive 329 | description: 330 | name: test_api 331 | url: "https://pub.dartlang.org" 332 | source: hosted 333 | version: "0.2.5" 334 | typed_data: 335 | dependency: transitive 336 | description: 337 | name: typed_data 338 | url: "https://pub.dartlang.org" 339 | source: hosted 340 | version: "1.1.6" 341 | vector_math: 342 | dependency: transitive 343 | description: 344 | name: vector_math 345 | url: "https://pub.dartlang.org" 346 | source: hosted 347 | version: "2.0.8" 348 | vibration: 349 | dependency: "direct main" 350 | description: 351 | name: vibration 352 | url: "https://pub.dartlang.org" 353 | source: hosted 354 | version: "1.2.2" 355 | wave: 356 | dependency: "direct main" 357 | description: 358 | name: wave 359 | url: "https://pub.dartlang.org" 360 | source: hosted 361 | version: "0.0.8" 362 | xml: 363 | dependency: transitive 364 | description: 365 | name: xml 366 | url: "https://pub.dartlang.org" 367 | source: hosted 368 | version: "3.5.0" 369 | yaml: 370 | dependency: transitive 371 | description: 372 | name: yaml 373 | url: "https://pub.dartlang.org" 374 | source: hosted 375 | version: "2.2.0" 376 | sdks: 377 | dart: ">=2.5.0 <3.0.0" 378 | flutter: ">=1.6.7 <2.0.0" 379 | -------------------------------------------------------------------------------- /lib/ui/multiplayer/multiplayer_result.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:esys_flutter_share/esys_flutter_share.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:screenshot/screenshot.dart'; 6 | import 'package:snaphunt/constants/app_theme.dart'; 7 | import 'package:snaphunt/data/repository.dart'; 8 | import 'package:snaphunt/model/player.dart'; 9 | import 'package:snaphunt/ui/home.dart'; 10 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 11 | import 'package:snaphunt/widgets/multiplayer/room_loading.dart'; 12 | 13 | class ResultMultiPlayer extends StatefulWidget { 14 | final String gameId; 15 | final String title; 16 | final int duration; 17 | 18 | const ResultMultiPlayer({ 19 | Key key, 20 | this.gameId, 21 | this.title, 22 | this.duration, 23 | }) : super(key: key); 24 | 25 | @override 26 | _ResultMultiPlayerState createState() => _ResultMultiPlayerState(); 27 | } 28 | 29 | class _ResultMultiPlayerState extends State { 30 | List _players; 31 | ScreenshotController screenshotController = ScreenshotController(); 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | initPlayers(); 37 | } 38 | 39 | void initPlayers() async { 40 | final players = await Repository.instance.getPlayers(widget.gameId); 41 | players.sort((a, b) => b.score.compareTo(a.score)); 42 | setState(() { 43 | _players = players; 44 | }); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return Scaffold( 50 | body: Container( 51 | color: Colors.white, 52 | child: Container( 53 | child: _players == null 54 | ? RoomLoading() 55 | : Column( 56 | children: [ 57 | Screenshot( 58 | controller: screenshotController, 59 | child: Container( 60 | color: Colors.white, 61 | width: double.infinity, 62 | child: Column( 63 | mainAxisAlignment: MainAxisAlignment.start, 64 | crossAxisAlignment: CrossAxisAlignment.center, 65 | mainAxisSize: MainAxisSize.max, 66 | children: [ 67 | ResultHeader( 68 | title: widget.title, 69 | duration: widget.duration, 70 | ), 71 | ResultWinner(winner: _players.first), 72 | Divider( 73 | color: Colors.grey, 74 | thickness: 2, 75 | indent: 0, 76 | endIndent: 0, 77 | ), 78 | ResultPlayers( 79 | players: _players.sublist(1, _players.length), 80 | ), 81 | ], 82 | ), 83 | ), 84 | ), 85 | const SizedBox(height: 40), 86 | FancyButton( 87 | color: Colors.orange, 88 | size: 50, 89 | child: Container( 90 | width: 150, 91 | child: Row( 92 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 93 | children: [ 94 | Padding( 95 | padding: const EdgeInsets.all(0), 96 | child: Icon(Icons.share, color: Colors.white), 97 | ), 98 | Text( 99 | 'Share', 100 | style: fancy_button_style, 101 | ) 102 | ], 103 | ), 104 | ), 105 | onPressed: () async { 106 | screenshotController.capture().then((File image) async { 107 | await Share.file( 108 | 'SnapHunt', 109 | 'snaphunt.png', 110 | image.readAsBytesSync().buffer.asUint8List(), 111 | 'image/png'); 112 | }).catchError((onError) { 113 | print(onError); 114 | }); 115 | }, 116 | ), 117 | const SizedBox(height: 20), 118 | FancyButton( 119 | color: Colors.deepOrange, 120 | size: 50, 121 | child: Container( 122 | width: 150, 123 | child: Row( 124 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 125 | children: [ 126 | Padding( 127 | padding: const EdgeInsets.all(0), 128 | child: 129 | Icon(Icons.arrow_back, color: Colors.white), 130 | ), 131 | Text( 132 | 'Return to Lobby', 133 | style: fancy_button_style, 134 | ) 135 | ], 136 | ), 137 | ), 138 | onPressed: () { 139 | Navigator.of(context).pop(); 140 | }, 141 | ), 142 | ], 143 | ), 144 | ), 145 | ), 146 | ); 147 | } 148 | } 149 | 150 | class ResultHeader extends StatelessWidget { 151 | final String title; 152 | final int duration; 153 | 154 | const ResultHeader({Key key, this.title, this.duration}) : super(key: key); 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | return Container( 159 | padding: EdgeInsets.fromLTRB( 160 | 16.0, MediaQuery.of(context).padding.top + 8, 16.0, 16.0), 161 | height: MediaQuery.of(context).size.height * 0.23, 162 | color: Colors.orange, 163 | child: Row( 164 | children: [ 165 | Expanded( 166 | child: ResultHeaderLogo(), 167 | ), 168 | Expanded( 169 | flex: 2, 170 | child: Column( 171 | mainAxisAlignment: MainAxisAlignment.center, 172 | crossAxisAlignment: CrossAxisAlignment.end, 173 | children: [ 174 | Text( 175 | title, 176 | style: TextStyle( 177 | color: Colors.white, 178 | fontSize: 32, 179 | fontWeight: FontWeight.bold, 180 | ), 181 | ), 182 | const SizedBox(height: 20), 183 | Text( 184 | '$duration min', 185 | style: TextStyle( 186 | color: Colors.red, 187 | fontSize: 18, 188 | fontWeight: FontWeight.w700, 189 | ), 190 | ), 191 | const SizedBox(height: 4), 192 | Text( 193 | 'Hunt Time', 194 | style: TextStyle( 195 | color: Colors.white, 196 | fontSize: 16, 197 | fontWeight: FontWeight.w500, 198 | ), 199 | ) 200 | ], 201 | ), 202 | ) 203 | ], 204 | ), 205 | ); 206 | } 207 | } 208 | 209 | class ResultHeaderLogo extends StatelessWidget { 210 | @override 211 | Widget build(BuildContext context) { 212 | return Material( 213 | elevation: 4.0, 214 | shape: const CircleBorder( 215 | side: BorderSide(color: Colors.transparent), 216 | ), 217 | child: Container( 218 | decoration: BoxDecoration( 219 | shape: BoxShape.circle, 220 | color: Colors.deepPurple[300], 221 | ), 222 | height: 120, 223 | width: 120, 224 | alignment: Alignment.center, 225 | child: Image.asset( 226 | 'assets/trophy.png', 227 | height: 80, 228 | ), 229 | ), 230 | ); 231 | } 232 | } 233 | 234 | class ResultWinner extends StatelessWidget { 235 | final Player winner; 236 | 237 | const ResultWinner({Key key, this.winner}) : super(key: key); 238 | 239 | @override 240 | Widget build(BuildContext context) { 241 | return Container( 242 | height: MediaQuery.of(context).size.height * 0.2, 243 | child: Row( 244 | mainAxisSize: MainAxisSize.min, 245 | children: [ 246 | Column( 247 | mainAxisAlignment: MainAxisAlignment.center, 248 | crossAxisAlignment: CrossAxisAlignment.start, 249 | children: [ 250 | UserAvatar( 251 | photoUrl: winner.user.photoUrl, 252 | height: 100, 253 | ), 254 | ], 255 | ), 256 | SizedBox(width: 20), 257 | Column( 258 | mainAxisAlignment: MainAxisAlignment.center, 259 | crossAxisAlignment: CrossAxisAlignment.start, 260 | children: [ 261 | Text( 262 | 'WINNER', 263 | style: TextStyle( 264 | color: Colors.red, 265 | fontSize: 32, 266 | fontWeight: FontWeight.bold, 267 | ), 268 | ), 269 | SizedBox(height: 20), 270 | Text( 271 | '${winner.user.displayName}', 272 | style: TextStyle( 273 | fontSize: 24, 274 | fontWeight: FontWeight.w500, 275 | ), 276 | ), 277 | const SizedBox(height: 10.0), 278 | Text( 279 | '${winner.score} points', 280 | style: TextStyle( 281 | color: Colors.orange, 282 | fontSize: 18, 283 | fontWeight: FontWeight.w300, 284 | ), 285 | ) 286 | ], 287 | ) 288 | ], 289 | ), 290 | ); 291 | } 292 | } 293 | 294 | class ResultPlayers extends StatelessWidget { 295 | final List players; 296 | 297 | const ResultPlayers({Key key, this.players}) : super(key: key); 298 | @override 299 | Widget build(BuildContext context) { 300 | return Container( 301 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 302 | height: MediaQuery.of(context).size.height * 0.25, 303 | child: ListView.builder( 304 | itemCount: players.length, 305 | itemBuilder: (context, index) { 306 | final player = players[index]; 307 | 308 | return ListTile( 309 | leading: UserAvatar( 310 | borderColor: user_colors[(index + 1) % 4], 311 | photoUrl: player.user.photoUrl, 312 | height: 50, 313 | ), 314 | title: Text( 315 | player.user.displayName, 316 | style: TextStyle( 317 | color: Colors.black, 318 | fontSize: 18, 319 | fontWeight: FontWeight.w400, 320 | ), 321 | ), 322 | trailing: Text( 323 | '${player.score} points', 324 | style: TextStyle( 325 | color: Colors.orange, 326 | fontSize: 16, 327 | fontWeight: FontWeight.w300, 328 | ), 329 | ), 330 | ); 331 | }, 332 | ), 333 | ); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /lib/ui/multiplayer/room.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:qr_flutter/qr_flutter.dart'; 4 | import 'package:snaphunt/constants/app_theme.dart'; 5 | import 'package:snaphunt/constants/game_status_enum.dart'; 6 | import 'package:snaphunt/routes.dart'; 7 | import 'package:snaphunt/stores/game_model.dart'; 8 | import 'package:snaphunt/ui/home.dart'; 9 | import 'package:snaphunt/utils/utils.dart'; 10 | import 'package:snaphunt/widgets/common/fancy_button.dart'; 11 | import 'package:snaphunt/widgets/multiplayer/host_start_button.dart'; 12 | import 'package:snaphunt/widgets/multiplayer/player_await_button.dart'; 13 | import 'package:snaphunt/widgets/multiplayer/room_exit_dialog.dart'; 14 | import 'package:snaphunt/widgets/multiplayer/room_loading.dart'; 15 | 16 | class Room extends StatelessWidget { 17 | void gameStatusListener(GameModel model, BuildContext context) { 18 | if (GameStatus.full == model.status) { 19 | Navigator.of(context).pop(); 20 | showAlertDialog( 21 | context: context, 22 | title: 'Room Full', 23 | body: 'Room already reached max players', 24 | ); 25 | } 26 | 27 | if (GameStatus.game == model.status) { 28 | Navigator.of(context).pushReplacementNamed( 29 | Router.game, 30 | arguments: [ 31 | model.game, 32 | model.userId, 33 | model.players, 34 | ], 35 | ); 36 | } 37 | 38 | if (GameStatus.cancelled == model.status) { 39 | Navigator.of(context).pop(); 40 | showAlertDialog( 41 | context: context, 42 | title: 'Game Cancelled', 43 | body: 'Game was cancelled by host!', 44 | ); 45 | } 46 | 47 | if (GameStatus.kicked == model.status) { 48 | Navigator.of(context).pop(); 49 | showAlertDialog( 50 | context: context, 51 | title: 'Kicked', 52 | body: 'You were kicked by the host!', 53 | ); 54 | } 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | bool _isHost = false; 60 | 61 | return WillPopScope( 62 | onWillPop: () async { 63 | final roomCode = await showDialog( 64 | context: context, 65 | barrierDismissible: false, 66 | builder: (BuildContext context) { 67 | return RoomExitDialog( 68 | title: _isHost ? 'Delete room' : 'Leave room', 69 | body: _isHost 70 | ? 'Leaving the room will cancel the game' 71 | : 'Are you sure you want to leave from the room?', 72 | ); 73 | }, 74 | ); 75 | 76 | return roomCode; 77 | }, 78 | child: Scaffold( 79 | appBar: AppBar( 80 | iconTheme: IconThemeData( 81 | color: Colors.white, 82 | ), 83 | title: Text( 84 | 'Room', 85 | style: TextStyle(color: Colors.white), 86 | ), 87 | centerTitle: true, 88 | elevation: 0, 89 | ), 90 | body: Container( 91 | child: Consumer( 92 | builder: (context, model, child) { 93 | _isHost = model.isHost; 94 | 95 | if (model.isLoading) { 96 | return RoomLoading(); 97 | } 98 | 99 | WidgetsBinding.instance.addPostFrameCallback( 100 | (_) => gameStatusListener(model, context), 101 | ); 102 | 103 | return child; 104 | }, 105 | child: Column( 106 | children: [ 107 | RoomDetails(), 108 | PlayerRow(), 109 | Expanded(child: PlayerList()), 110 | ], 111 | ), 112 | ), 113 | ), 114 | ), 115 | ); 116 | } 117 | } 118 | 119 | class RoomDetails extends StatelessWidget { 120 | const RoomDetails({Key key}) : super(key: key); 121 | 122 | @override 123 | Widget build(BuildContext context) { 124 | return Consumer(builder: (context, model, child) { 125 | return Container( 126 | height: MediaQuery.of(context).size.height * 0.35, 127 | width: double.infinity, 128 | // padding: const EdgeInsets.fromLTRB(16.0, 32.0, 16.0, 32.0), 129 | child: Column( 130 | mainAxisSize: MainAxisSize.max, 131 | mainAxisAlignment: MainAxisAlignment.start, 132 | crossAxisAlignment: CrossAxisAlignment.center, 133 | children: [ 134 | Row( 135 | children: [ 136 | Container( 137 | color: Color(0xFEEBEBEB), 138 | width: MediaQuery.of(context).size.width / 2.75, 139 | height: 255, 140 | child: Column( 141 | crossAxisAlignment: CrossAxisAlignment.center, 142 | children: [ 143 | const SizedBox(height: 20), 144 | QrImage( 145 | data: model.game.id, 146 | version: QrVersions.auto, 147 | size: 120.0, 148 | padding: EdgeInsets.all(10), 149 | backgroundColor: Colors.white, 150 | ), 151 | const SizedBox(height: 32), 152 | Text( 153 | '${model.game.timeLimit} min', 154 | style: TextStyle( 155 | fontSize: 32, 156 | color: Colors.red, 157 | fontWeight: FontWeight.bold), 158 | ), 159 | const SizedBox(height: 10), 160 | Text( 161 | 'Hunt time', 162 | style: TextStyle( 163 | fontWeight: FontWeight.bold, 164 | fontSize: 24, 165 | ), 166 | ), 167 | ], 168 | )), 169 | Container( 170 | color: Colors.white, 171 | width: MediaQuery.of(context).size.width / 1.575, 172 | padding: EdgeInsets.fromLTRB(15, 25, 15, 5), 173 | height: 255, 174 | child: Column( 175 | mainAxisAlignment: MainAxisAlignment.center, 176 | children: [ 177 | Text( 178 | model.game.name, 179 | overflow: TextOverflow.ellipsis, 180 | maxLines: 2, 181 | textAlign: TextAlign.center, 182 | style: TextStyle( 183 | fontSize: 38, 184 | height: 0.8, 185 | fontWeight: FontWeight.bold, 186 | ), 187 | ), 188 | const SizedBox(height: 25), 189 | Text('Code:', style: TextStyle(fontSize: 24, color: Colors.grey),), 190 | const SizedBox(height: 10), 191 | Text( 192 | model.game.id, 193 | style: TextStyle( 194 | fontWeight: FontWeight.bold, 195 | fontSize: 20, 196 | ), 197 | ), 198 | const SizedBox(height: 40), 199 | RoomBody(), 200 | ], 201 | )), 202 | ], 203 | ) 204 | ], 205 | ), 206 | ); 207 | }); 208 | } 209 | } 210 | 211 | class RoomBody extends StatelessWidget { 212 | const RoomBody({Key key}) : super(key: key); 213 | 214 | @override 215 | Widget build(BuildContext context) { 216 | return Consumer( 217 | builder: (context, model, child) { 218 | if (!model.isHost) { 219 | return PlayerAwaitButton( 220 | canStartGame: model.canStartGame, 221 | ); 222 | } 223 | 224 | return HostStartButton( 225 | canStartGame: model.canStartGame, 226 | onGameStart: model.onGameStart, 227 | ); 228 | }, 229 | ); 230 | } 231 | } 232 | 233 | class PlayerRow extends StatelessWidget { 234 | const PlayerRow({Key key}) : super(key: key); 235 | 236 | @override 237 | Widget build(BuildContext context) { 238 | return Consumer( 239 | builder: (context, model, child) { 240 | return Container( 241 | color: Colors.grey[600], 242 | height: 45, 243 | padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24.0), 244 | child: Row( 245 | mainAxisSize: MainAxisSize.max, 246 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 247 | children: [ 248 | Text( 249 | 'PLAYERS', 250 | style: TextStyle( 251 | color: Colors.white, 252 | fontWeight: FontWeight.bold, 253 | fontSize: 20, 254 | ), 255 | ), 256 | Text( 257 | '${model.players.length}/${model.game.maxPlayers}', 258 | style: TextStyle( 259 | color: Colors.white, 260 | fontWeight: FontWeight.bold, 261 | fontSize: 20, 262 | ), 263 | ), 264 | ], 265 | ), 266 | ); 267 | }, 268 | ); 269 | } 270 | } 271 | 272 | class PlayerList extends StatelessWidget { 273 | const PlayerList({Key key}) : super(key: key); 274 | 275 | @override 276 | Widget build(BuildContext context) { 277 | return Consumer( 278 | builder: (context, model, child) { 279 | return Container( 280 | child: ListView.builder( 281 | itemCount: model.players.length, 282 | itemBuilder: (context, index) { 283 | final player = model.players[index]; 284 | final isMe = player.user.uid == model.userId; 285 | 286 | return Container( 287 | padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), 288 | child: ListTile( 289 | title: Text( 290 | player.user.displayName, 291 | style: TextStyle( 292 | fontSize: 24, 293 | fontWeight: FontWeight.bold, 294 | ), 295 | ), 296 | leading: UserAvatar( 297 | photoUrl: player.user.photoUrl, 298 | height: 45, 299 | borderColor: user_colors[index % 4], 300 | ), 301 | trailing: model.isHost 302 | ? !isMe 303 | ? FancyButton( 304 | child: Text( 305 | 'REMOVE', 306 | style: fancy_button_style, 307 | ), 308 | color: Colors.red, 309 | size: 40, 310 | onPressed: () => 311 | model.onKickPlayer(player.user.uid), 312 | ) 313 | : Container( 314 | height: 0, 315 | width: 0, 316 | ) 317 | : Container( 318 | height: 0, 319 | width: 0, 320 | ), 321 | ), 322 | ); 323 | }, 324 | ), 325 | ); 326 | }, 327 | ); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /lib/widgets/common/hunt_game.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_size_text/auto_size_text.dart'; 2 | import 'package:expandable/expandable.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:snaphunt/constants/app_theme.dart'; 6 | import 'package:snaphunt/model/hunt.dart'; 7 | import 'package:snaphunt/model/player.dart'; 8 | import 'package:snaphunt/routes.dart'; 9 | import 'package:snaphunt/stores/hunt_model.dart'; 10 | import 'package:snaphunt/stores/player_hunt_model.dart'; 11 | import 'package:snaphunt/ui/home.dart'; 12 | import 'package:snaphunt/utils/utils.dart'; 13 | import 'package:snaphunt/widgets/common/camera.dart'; 14 | import 'package:snaphunt/widgets/common/countdown.dart'; 15 | import 'package:snaphunt/widgets/multiplayer/room_exit_dialog.dart'; 16 | 17 | class HuntGame extends StatelessWidget { 18 | final String title; 19 | final List players; 20 | 21 | const HuntGame({Key key, this.title, this.players}) : super(key: key); 22 | 23 | void statusListener(HuntModel model, BuildContext context) { 24 | if (model.isMultiplayer) { 25 | if (model.isTimeUp) { 26 | pushMultiPlayerResult( 27 | model, 28 | context, 29 | title: 'Times up!', 30 | body: 'Times up! Hunting stops now!', 31 | gameTitle: title, 32 | duration: model.timeDuration, 33 | ); 34 | } else if (model.isGameEnd && model.isHuntComplete) { 35 | pushMultiPlayerResult( 36 | model, 37 | context, 38 | title: 'Congrats!', 39 | body: 'All items found! You are a champion!', 40 | gameTitle: title, 41 | duration: model.timeDuration, 42 | ); 43 | } else if (model.isGameEnd && !model.isHuntComplete) { 44 | pushMultiPlayerResult( 45 | model, 46 | context, 47 | title: 'Hunt Ended!', 48 | body: 'Someone completed the hunt!', 49 | gameTitle: title, 50 | duration: model.timeDuration, 51 | ); 52 | } 53 | } else { 54 | if (model.isTimeUp) { 55 | pushSinglePlayerResult( 56 | model, 57 | context, 58 | title: 'Times up!', 59 | body: 'Times up! Hunting stops now!', 60 | ); 61 | } else if (model.isHuntComplete) { 62 | pushSinglePlayerResult( 63 | model, 64 | context, 65 | title: 'Congrats!', 66 | body: 'All items found! You are a champion!', 67 | ); 68 | } 69 | } 70 | } 71 | 72 | void pushSinglePlayerResult( 73 | HuntModel model, 74 | BuildContext context, { 75 | String title, 76 | String body, 77 | }) { 78 | Navigator.of(context).pushReplacementNamed( 79 | Router.resultSingle, 80 | arguments: [ 81 | model.isHuntComplete, 82 | model.objects, 83 | model.duration.elapsed, 84 | ], 85 | ); 86 | 87 | showAlertDialog( 88 | context: context, 89 | title: title, 90 | body: body, 91 | ); 92 | } 93 | 94 | void pushMultiPlayerResult( 95 | HuntModel model, 96 | BuildContext context, { 97 | String title, 98 | String body, 99 | String gameTitle, 100 | int duration, 101 | }) { 102 | Navigator.of(context).pushReplacementNamed( 103 | Router.resultMulti, 104 | arguments: [model.gameId, gameTitle, duration], 105 | ); 106 | 107 | showAlertDialog( 108 | context: context, 109 | title: title, 110 | body: body, 111 | ); 112 | } 113 | 114 | @override 115 | Widget build(BuildContext context) { 116 | return WillPopScope( 117 | onWillPop: () async { 118 | final roomCode = await showDialog( 119 | context: context, 120 | barrierDismissible: false, 121 | builder: (BuildContext context) { 122 | return RoomExitDialog( 123 | title: 'Leave game?', 124 | body: 125 | 'Are you sure you want to leave the game? Your progress will be lost', 126 | ); 127 | }, 128 | ); 129 | 130 | return roomCode; 131 | }, 132 | child: Scaffold( 133 | body: Consumer( 134 | builder: (context, model, child) { 135 | WidgetsBinding.instance.addPostFrameCallback( 136 | (_) => statusListener(model, context), 137 | ); 138 | 139 | return Stack( 140 | children: [ 141 | child, 142 | HeaderRow( 143 | title: title, 144 | timeLimit: model.timeLimit, 145 | ), 146 | Container( 147 | margin: const EdgeInsets.only(top: 90), 148 | child: Column( 149 | mainAxisSize: MainAxisSize.min, 150 | children: [ 151 | if (model.isMultiplayer) 152 | PlayerUpdate( 153 | gameId: model.gameId, 154 | players: players, 155 | ), 156 | ExpandedWordList( 157 | objectsFound: model.objectsFound, 158 | totalObjects: model.objects.length, 159 | hunt: model.nextNotFound, 160 | ), 161 | ], 162 | ), 163 | ), 164 | ], 165 | ); 166 | }, 167 | child: CameraScreen(), 168 | ), 169 | ), 170 | ); 171 | } 172 | } 173 | 174 | class HeaderRow extends StatelessWidget { 175 | final String title; 176 | final DateTime timeLimit; 177 | 178 | const HeaderRow({Key key, this.title, this.timeLimit}) : super(key: key); 179 | 180 | @override 181 | Widget build(BuildContext context) { 182 | return Container( 183 | height: 75, 184 | padding: EdgeInsets.only( 185 | top: MediaQuery.of(context).padding.top + 8.0, 186 | left: 24.0, 187 | right: 24.0, 188 | ), 189 | child: Row( 190 | crossAxisAlignment: CrossAxisAlignment.center, 191 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 192 | children: [ 193 | Text( 194 | title, 195 | style: TextStyle( 196 | color: Colors.white, 197 | fontSize: 24, 198 | ), 199 | ), 200 | CountDownTimer(timeLimit: timeLimit) 201 | ], 202 | ), 203 | ); 204 | } 205 | } 206 | 207 | class ExpandedWordList extends StatelessWidget { 208 | final int objectsFound; 209 | 210 | final int totalObjects; 211 | 212 | final Hunt hunt; 213 | 214 | const ExpandedWordList({ 215 | Key key, 216 | this.objectsFound, 217 | this.totalObjects, 218 | this.hunt, 219 | }) : super(key: key); 220 | 221 | @override 222 | Widget build(BuildContext context) { 223 | return Container( 224 | child: ExpandablePanel( 225 | collapsed: Container( 226 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 227 | child: Row( 228 | crossAxisAlignment: CrossAxisAlignment.center, 229 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 230 | children: [ 231 | Text( 232 | 'Items ($objectsFound/$totalObjects)', 233 | style: TextStyle( 234 | fontSize: 20, 235 | color: Colors.white, 236 | ), 237 | ), 238 | if (hunt != null) WordTile(word: hunt) 239 | ], 240 | ), 241 | ), 242 | expanded: WordList(), 243 | headerAlignment: ExpandablePanelHeaderAlignment.center, 244 | tapBodyToCollapse: true, 245 | tapHeaderToExpand: true, 246 | hasIcon: true, 247 | iconColor: Colors.white, 248 | ), 249 | ); 250 | } 251 | } 252 | 253 | class WordList extends StatelessWidget { 254 | @override 255 | Widget build(BuildContext context) { 256 | final size = MediaQuery.of(context).size; 257 | 258 | return Consumer( 259 | builder: (context, model, child) { 260 | return Container( 261 | width: double.infinity, 262 | color: Colors.transparent, 263 | constraints: BoxConstraints(maxHeight: size.height * 0.4), 264 | padding: const EdgeInsets.only(left: 16.0), 265 | child: ListView( 266 | shrinkWrap: true, 267 | children: [ 268 | Wrap( 269 | alignment: WrapAlignment.center, 270 | spacing: 24, 271 | runSpacing: 14, 272 | direction: Axis.horizontal, 273 | children: [ 274 | for (Hunt word in model.objects) 275 | WordTile( 276 | word: word, 277 | ), 278 | ], 279 | ) 280 | ], 281 | ), 282 | ); 283 | }, 284 | ); 285 | } 286 | } 287 | 288 | class WordTile extends StatelessWidget { 289 | final Hunt word; 290 | 291 | const WordTile({Key key, this.word}) : super(key: key); 292 | 293 | @override 294 | Widget build(BuildContext context) { 295 | return Container( 296 | child: Row( 297 | mainAxisSize: MainAxisSize.min, 298 | mainAxisAlignment: MainAxisAlignment.center, 299 | children: [ 300 | Icon( 301 | word.isFound ? Icons.check : Icons.fiber_manual_record, 302 | color: word.isFound ? Colors.green : Colors.red, 303 | ), 304 | SizedBox(width: 10), 305 | AutoSizeText( 306 | word.word, 307 | maxLines: 1, 308 | minFontSize: 10, 309 | style: TextStyle( 310 | fontSize: 20, 311 | color: Colors.white, 312 | decoration: word.isFound ? TextDecoration.lineThrough : null, 313 | ), 314 | ), 315 | ], 316 | ), 317 | ); 318 | } 319 | } 320 | 321 | class PlayerUpdate extends StatelessWidget { 322 | final String gameId; 323 | 324 | final List players; 325 | 326 | const PlayerUpdate({Key key, this.gameId, this.players}) : super(key: key); 327 | 328 | @override 329 | Widget build(BuildContext context) { 330 | return ChangeNotifierProvider( 331 | builder: (_) => PlayHuntModel( 332 | gameId: gameId, 333 | players: players, 334 | ), 335 | child: Consumer( 336 | builder: (context, model, child) { 337 | return Container( 338 | margin: const EdgeInsets.all(8), 339 | height: 60, 340 | child: ListView.builder( 341 | scrollDirection: Axis.horizontal, 342 | itemCount: model.players.length, 343 | itemBuilder: (context, index) { 344 | final player = model.players[index]; 345 | return ScoreAvatar( 346 | photoUrl: player.user.photoUrl, 347 | score: player.score, 348 | userBorderColor: user_colors[index % 4], 349 | ); 350 | }, 351 | ), 352 | ); 353 | }, 354 | ), 355 | ); 356 | } 357 | } 358 | 359 | class ScoreAvatar extends StatelessWidget { 360 | final String photoUrl; 361 | final int score; 362 | final Color userBorderColor; 363 | 364 | const ScoreAvatar({Key key, this.photoUrl, this.score, this.userBorderColor}) 365 | : super(key: key); 366 | 367 | @override 368 | Widget build(BuildContext context) { 369 | return Container( 370 | margin: const EdgeInsets.symmetric(horizontal: 8.0), 371 | child: Row( 372 | mainAxisSize: MainAxisSize.min, 373 | crossAxisAlignment: CrossAxisAlignment.center, 374 | children: [ 375 | UserAvatar( 376 | borderColor: userBorderColor, 377 | photoUrl: photoUrl, 378 | height: 50, 379 | ), 380 | SizedBox(width: 10), 381 | Text( 382 | '$score', 383 | style: TextStyle( 384 | color: Colors.white, 385 | fontSize: 24, 386 | fontWeight: FontWeight.bold, 387 | ), 388 | ), 389 | ], 390 | ), 391 | ); 392 | } 393 | } 394 | 395 | class AvatarScore extends StatelessWidget { 396 | final int score; 397 | 398 | const AvatarScore({Key key, this.score}) : super(key: key); 399 | @override 400 | Widget build(BuildContext context) { 401 | return Container( 402 | height: 20, 403 | width: 20, 404 | decoration: BoxDecoration( 405 | color: Colors.white, 406 | shape: BoxShape.circle, 407 | ), 408 | alignment: Alignment.center, 409 | padding: const EdgeInsets.all(2), 410 | child: Text('$score'), 411 | ); 412 | } 413 | } 414 | --------------------------------------------------------------------------------