├── 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 |
7 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------