Add firebase support to web_dashboard (#421)

* add mock data, app state, model classes

* Set up app without ChangeNotifier

* refactor

* add experiments to experimental/

* Add project-agnostic Firebase authentication code

* add sign in button

* add stub firebase API

* add firestore

* refactor code for google_sign_in

* update pubspec.lock

* switch to mocks for non-firebase version

* Add firebase instructions to the README

* fix README

* sign in silently if the user is already signed in

* add json_serializable

* update README

* ignore 'id' field on types

* Implement FirebaseItemApi

* Add build_runner instructions to README

* remove experiments directory

* add EditItemForm

* move types.dart into api.dart

* move mock and firebase configuration into the constructor

* add main_mock entrypoint

* add copyright checks to grinder script

* fix fix-copyright task

* run grind fix-copyright

* add run and generate tasks

* add run tasks to grind script

* add fillWithMockData() fix delete() in mock API

* add edit / new form dialogs

* Add charts that display entries from Firebase

* Add Entries list without editing

* refactor home page

* format

* Add entries page functionality

* Show current day in charts

* cleanup: pubspec.lock, remove type annotation

* Remove _selectedItem from Home page

Add ItemsDropdown
Use ItemsDropdown in NewEntryDialog / NewEntryForm

* rename item-category

* don't wait to show snackbar on delete

* fix circular progress indicator

* Move dialogs into dialogs.dart

* run grind fix-copyright

* remove unused import

* Refactor entry total calculation, add chart_utils library

* fix bug in chart_utils.dart

* convert CategoryChart to a stateless widget

* use a const for number of days in chart

* code review updates

- rename stream -> subscribe
- timeStamp -> timestamp
- remove latest() from API
- use FutureBuilder and StreamBuilder instead of stateful widget
- rename variables in mock_service_test.dart

* use a single collection reference in firebase API

* remove reference to stream in mock API

* Use a new type,  _EntriesEvent to improve filtering in mock API

* add analysis_options.yaml and fix (most) issues

* fix avoid_types_on_closure_parameters lint warnings

* use spread operator in dashboard.dart

* handle case where selected item in the category dropdown goes away

* use StreamBuilder + FutureBuilder on Entries page

* rename method

* use fake firebase configuration

* update pubspec.lock

* update README

* Change categories_dropdown to FutureBuilder + StreamBuilder

* Update minSdkVersion in build.gradle

SDK version 16 was failing: "The number of method references in a .dex
file cannot exceed 64K."

* update README

* Use a collection reference in FirebaseEntryApi

Already added to FirebaseCategoryApi

* Invoke onSelected in CategoriesDropdown when necessary

Also, avoid calling onSelected during a build.

* fix misnamed var

* remove unused import

* Use relative imports

* Use extension methods for DateTime utilities

* remove forms.dart

* Make Firebase instructions specific for this sample

* add copyright headers

* fix grammar

* dartfmt

* avoid setState() during build phase in CategoryDropdown

* add empty test to material_theme_builder
pull/450/head
John Ryan 5 years ago committed by GitHub
parent b518c322cc
commit 395ae8c0bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -30,3 +30,94 @@ Skia / CanvasKit mode:
flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true
```
## Running JSON code generator
```
flutter pub run grinder generate
```
## Add Firebase
### Step 1: Create a new Firebase project
Go to [console.firebase.google.com](https://console.firebase.google.com/) and
create a new Firebase project.
### Step 2: Enable Google Sign In for your project
In the Firebase console, go to "Authentication" and enable Google sign in. Click
on "Web SDK Configuration" and copy down your Web client ID.
### Step 3: Add Client ID to `index.html`
Uncomment this line in `index.html` and replace `<YOUR WEB CLIENT ID>` with the
client ID from Step 2:
```html
<!-- Uncomment and add Firebase client ID here: -->
<!-- <meta name="google-signin-client_id" content="<YOUR WEB CLIENT ID>"> -->
```
### Step 4: Create a web app
In the Firebase console, under "Project overview", click "Add app", select Web,
and replace the contents of `web/firebase_init.js`.
```javascript
// web/firebase_init.js
var firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
```
### Step 4: Create Cloud Firestore
Create a new Cloud Firestore database and add the following rules to disallow
users from reading/writing other users' data:
```
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId}/{document=**} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
```
### Step 5: Run the app
Run the app on port 5000:
```bash
flutter run -d chrome --web-port=5000
```
If you see CORS errors in your browser's console, go to the [Services
section][cloud-console-apis] in the Google Cloud console, go to Credentials, and
verify that `localhost:5000` is whitelisted.
### (optional) Step 7: Set up iOS and Android
If you would like to run the app on iOS or Android, make sure you've installed
the appropriate configuration files described at
[firebase.google.com/docs/flutter/setup][flutter-setup] from step 1, and follow
the instructions detailed in the [google_sign_in README][google-sign-in]
[flutter-setup]: https://firebase.google.com/docs/flutter/setup
[cloud-console-apis]: https://console.developers.google.com/apis/dashboard
[google-sign-in]: https://pub.dev/packages/google_sign_in

@ -0,0 +1,31 @@
include: package:pedantic/analysis_options.1.8.0.yaml
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
linter:
rules:
- avoid_types_on_closure_parameters
- avoid_void_async
- await_only_futures
- camel_case_types
- cancel_subscriptions
- close_sinks
- constant_identifier_names
- control_flow_in_finally
- directives_ordering
- empty_statements
- hash_and_equals
- implementation_imports
- non_constant_identifier_names
- package_api_docs
- package_names
- package_prefixed_library_names
- test_types_in_equals
- throw_in_finally
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_new
- unnecessary_statements

@ -39,7 +39,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dev.flutter.web_dashboard"
minSdkVersion 16
minSdkVersion 21
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

@ -1 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

@ -1 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

@ -0,0 +1,87 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end
end
generated_key_values
end
target 'Runner' do
use_frameworks!
use_modular_headers!
# Flutter Pod
copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
end
end
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
# Plugin Pods
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end

@ -0,0 +1,410 @@
PODS:
- abseil/algorithm (0.20190808):
- abseil/algorithm/algorithm (= 0.20190808)
- abseil/algorithm/container (= 0.20190808)
- abseil/algorithm/algorithm (0.20190808)
- abseil/algorithm/container (0.20190808):
- abseil/algorithm/algorithm
- abseil/base/core_headers
- abseil/meta/type_traits
- abseil/base (0.20190808):
- abseil/base/atomic_hook (= 0.20190808)
- abseil/base/base (= 0.20190808)
- abseil/base/base_internal (= 0.20190808)
- abseil/base/bits (= 0.20190808)
- abseil/base/config (= 0.20190808)
- abseil/base/core_headers (= 0.20190808)
- abseil/base/dynamic_annotations (= 0.20190808)
- abseil/base/endian (= 0.20190808)
- abseil/base/log_severity (= 0.20190808)
- abseil/base/malloc_internal (= 0.20190808)
- abseil/base/pretty_function (= 0.20190808)
- abseil/base/spinlock_wait (= 0.20190808)
- abseil/base/throw_delegate (= 0.20190808)
- abseil/base/atomic_hook (0.20190808)
- abseil/base/base (0.20190808):
- abseil/base/atomic_hook
- abseil/base/base_internal
- abseil/base/config
- abseil/base/core_headers
- abseil/base/dynamic_annotations
- abseil/base/log_severity
- abseil/base/spinlock_wait
- abseil/meta/type_traits
- abseil/base/base_internal (0.20190808):
- abseil/meta/type_traits
- abseil/base/bits (0.20190808):
- abseil/base/core_headers
- abseil/base/config (0.20190808)
- abseil/base/core_headers (0.20190808):
- abseil/base/config
- abseil/base/dynamic_annotations (0.20190808)
- abseil/base/endian (0.20190808):
- abseil/base/config
- abseil/base/core_headers
- abseil/base/log_severity (0.20190808):
- abseil/base/core_headers
- abseil/base/malloc_internal (0.20190808):
- abseil/base/base
- abseil/base/config
- abseil/base/core_headers
- abseil/base/dynamic_annotations
- abseil/base/spinlock_wait
- abseil/base/pretty_function (0.20190808)
- abseil/base/spinlock_wait (0.20190808):
- abseil/base/core_headers
- abseil/base/throw_delegate (0.20190808):
- abseil/base/base
- abseil/base/config
- abseil/memory (0.20190808):
- abseil/memory/memory (= 0.20190808)
- abseil/memory/memory (0.20190808):
- abseil/base/core_headers
- abseil/meta/type_traits
- abseil/meta (0.20190808):
- abseil/meta/type_traits (= 0.20190808)
- abseil/meta/type_traits (0.20190808):
- abseil/base/config
- abseil/numeric/int128 (0.20190808):
- abseil/base/config
- abseil/base/core_headers
- abseil/strings/internal (0.20190808):
- abseil/base/core_headers
- abseil/base/endian
- abseil/meta/type_traits
- abseil/strings/strings (0.20190808):
- abseil/base/base
- abseil/base/bits
- abseil/base/config
- abseil/base/core_headers
- abseil/base/endian
- abseil/base/throw_delegate
- abseil/memory/memory
- abseil/meta/type_traits
- abseil/numeric/int128
- abseil/strings/internal
- abseil/time (0.20190808):
- abseil/time/internal (= 0.20190808)
- abseil/time/time (= 0.20190808)
- abseil/time/internal (0.20190808):
- abseil/time/internal/cctz (= 0.20190808)
- abseil/time/internal/cctz (0.20190808):
- abseil/time/internal/cctz/civil_time (= 0.20190808)
- abseil/time/internal/cctz/includes (= 0.20190808)
- abseil/time/internal/cctz/time_zone (= 0.20190808)
- abseil/time/internal/cctz/civil_time (0.20190808)
- abseil/time/internal/cctz/includes (0.20190808)
- abseil/time/internal/cctz/time_zone (0.20190808):
- abseil/time/internal/cctz/civil_time
- abseil/time/time (0.20190808):
- abseil/base/base
- abseil/base/core_headers
- abseil/numeric/int128
- abseil/strings/strings
- abseil/time/internal/cctz/civil_time
- abseil/time/internal/cctz/time_zone
- abseil/types (0.20190808):
- abseil/types/any (= 0.20190808)
- abseil/types/bad_any_cast (= 0.20190808)
- abseil/types/bad_any_cast_impl (= 0.20190808)
- abseil/types/bad_optional_access (= 0.20190808)
- abseil/types/bad_variant_access (= 0.20190808)
- abseil/types/compare (= 0.20190808)
- abseil/types/optional (= 0.20190808)
- abseil/types/span (= 0.20190808)
- abseil/types/variant (= 0.20190808)
- abseil/types/any (0.20190808):
- abseil/base/config
- abseil/base/core_headers
- abseil/meta/type_traits
- abseil/types/bad_any_cast
- abseil/utility/utility
- abseil/types/bad_any_cast (0.20190808):
- abseil/base/config
- abseil/types/bad_any_cast_impl
- abseil/types/bad_any_cast_impl (0.20190808):
- abseil/base/base
- abseil/base/config
- abseil/types/bad_optional_access (0.20190808):
- abseil/base/base
- abseil/base/config
- abseil/types/bad_variant_access (0.20190808):
- abseil/base/base
- abseil/base/config
- abseil/types/compare (0.20190808):
- abseil/base/core_headers
- abseil/meta/type_traits
- abseil/types/optional (0.20190808):
- abseil/base/base_internal
- abseil/base/config
- abseil/base/core_headers
- abseil/memory/memory
- abseil/meta/type_traits
- abseil/types/bad_optional_access
- abseil/utility/utility
- abseil/types/span (0.20190808):
- abseil/algorithm/algorithm
- abseil/base/core_headers
- abseil/base/throw_delegate
- abseil/meta/type_traits
- abseil/types/variant (0.20190808):
- abseil/base/base_internal
- abseil/base/config
- abseil/base/core_headers
- abseil/meta/type_traits
- abseil/types/bad_variant_access
- abseil/utility/utility
- abseil/utility/utility (0.20190808):
- abseil/base/base_internal
- abseil/base/config
- abseil/meta/type_traits
- AppAuth (1.3.0):
- AppAuth/Core (= 1.3.0)
- AppAuth/ExternalUserAgent (= 1.3.0)
- AppAuth/Core (1.3.0)
- AppAuth/ExternalUserAgent (1.3.0)
- BoringSSL-GRPC (0.0.3):
- BoringSSL-GRPC/Implementation (= 0.0.3)
- BoringSSL-GRPC/Interface (= 0.0.3)
- BoringSSL-GRPC/Implementation (0.0.3):
- BoringSSL-GRPC/Interface (= 0.0.3)
- BoringSSL-GRPC/Interface (0.0.3)
- cloud_firestore (0.0.1):
- Firebase/Core
- Firebase/Firestore (~> 6.0)
- Flutter
- cloud_firestore_web (0.1.0):
- Flutter
- Firebase/Auth (6.18.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 6.4.3)
- Firebase/Core (6.18.0):
- Firebase/CoreOnly
- FirebaseAnalytics (= 6.3.0)
- Firebase/CoreOnly (6.18.0):
- FirebaseCore (= 6.6.3)
- Firebase/Firestore (6.18.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 1.11.0)
- firebase_auth (0.0.1):
- Firebase/Auth (~> 6.3)
- Firebase/Core
- Flutter
- firebase_auth_web (0.1.0):
- Flutter
- firebase_core (0.0.1):
- Firebase/Core
- Flutter
- firebase_core_web (0.1.0):
- Flutter
- FirebaseAnalytics (6.3.0):
- FirebaseCore (~> 6.6)
- FirebaseInstallations (~> 1.1)
- GoogleAppMeasurement (= 6.3.0)
- GoogleUtilities/AppDelegateSwizzler (~> 6.0)
- GoogleUtilities/MethodSwizzler (~> 6.0)
- GoogleUtilities/Network (~> 6.0)
- "GoogleUtilities/NSData+zlib (~> 6.0)"
- nanopb (= 0.3.9011)
- FirebaseAuth (6.4.3):
- FirebaseAuthInterop (~> 1.0)
- FirebaseCore (~> 6.6)
- GoogleUtilities/AppDelegateSwizzler (~> 6.5)
- GoogleUtilities/Environment (~> 6.5)
- GTMSessionFetcher/Core (~> 1.1)
- FirebaseAuthInterop (1.0.0)
- FirebaseCore (6.6.3):
- FirebaseCoreDiagnostics (~> 1.2)
- FirebaseCoreDiagnosticsInterop (~> 1.2)
- GoogleUtilities/Environment (~> 6.5)
- GoogleUtilities/Logger (~> 6.5)
- FirebaseCoreDiagnostics (1.2.1):
- FirebaseCoreDiagnosticsInterop (~> 1.2)
- GoogleDataTransportCCTSupport (~> 1.3)
- GoogleUtilities/Environment (~> 6.5)
- GoogleUtilities/Logger (~> 6.5)
- nanopb (~> 0.3.901)
- FirebaseCoreDiagnosticsInterop (1.2.0)
- FirebaseFirestore (1.11.0):
- abseil/algorithm (= 0.20190808)
- abseil/base (= 0.20190808)
- abseil/memory (= 0.20190808)
- abseil/meta (= 0.20190808)
- abseil/strings/strings (= 0.20190808)
- abseil/time (= 0.20190808)
- abseil/types (= 0.20190808)
- FirebaseAuthInterop (~> 1.0)
- FirebaseCore (~> 6.2)
- "gRPC-C++ (= 0.0.9)"
- leveldb-library (~> 1.22)
- nanopb (~> 0.3.901)
- FirebaseInstallations (1.1.0):
- FirebaseCore (~> 6.6)
- GoogleUtilities/UserDefaults (~> 6.5)
- PromisesObjC (~> 1.2)
- Flutter (1.0.0)
- google_sign_in (0.0.1):
- Flutter
- GoogleSignIn (~> 5.0)
- google_sign_in_web (0.8.1):
- Flutter
- GoogleAppMeasurement (6.3.0):
- GoogleUtilities/AppDelegateSwizzler (~> 6.0)
- GoogleUtilities/MethodSwizzler (~> 6.0)
- GoogleUtilities/Network (~> 6.0)
- "GoogleUtilities/NSData+zlib (~> 6.0)"
- nanopb (= 0.3.9011)
- GoogleDataTransport (4.0.1)
- GoogleDataTransportCCTSupport (1.4.1):
- GoogleDataTransport (~> 4.0)
- nanopb (~> 0.3.901)
- GoogleSignIn (5.0.2):
- AppAuth (~> 1.2)
- GTMAppAuth (~> 1.0)
- GTMSessionFetcher/Core (~> 1.1)
- GoogleUtilities/AppDelegateSwizzler (6.5.1):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Environment (6.5.1)
- GoogleUtilities/Logger (6.5.1):
- GoogleUtilities/Environment
- GoogleUtilities/MethodSwizzler (6.5.1):
- GoogleUtilities/Logger
- GoogleUtilities/Network (6.5.1):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (6.5.1)"
- GoogleUtilities/Reachability (6.5.1):
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (6.5.1):
- GoogleUtilities/Logger
- "gRPC-C++ (0.0.9)":
- "gRPC-C++/Implementation (= 0.0.9)"
- "gRPC-C++/Interface (= 0.0.9)"
- "gRPC-C++/Implementation (0.0.9)":
- "gRPC-C++/Interface (= 0.0.9)"
- gRPC-Core (= 1.21.0)
- nanopb (~> 0.3)
- "gRPC-C++/Interface (0.0.9)"
- gRPC-Core (1.21.0):
- gRPC-Core/Implementation (= 1.21.0)
- gRPC-Core/Interface (= 1.21.0)
- gRPC-Core/Implementation (1.21.0):
- BoringSSL-GRPC (= 0.0.3)
- gRPC-Core/Interface (= 1.21.0)
- nanopb (~> 0.3)
- gRPC-Core/Interface (1.21.0)
- GTMAppAuth (1.0.0):
- AppAuth/Core (~> 1.0)
- GTMSessionFetcher (~> 1.1)
- GTMSessionFetcher (1.3.1):
- GTMSessionFetcher/Full (= 1.3.1)
- GTMSessionFetcher/Core (1.3.1)
- GTMSessionFetcher/Full (1.3.1):
- GTMSessionFetcher/Core (= 1.3.1)
- leveldb-library (1.22)
- nanopb (0.3.9011):
- nanopb/decode (= 0.3.9011)
- nanopb/encode (= 0.3.9011)
- nanopb/decode (0.3.9011)
- nanopb/encode (0.3.9011)
- PromisesObjC (1.2.8)
DEPENDENCIES:
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
- cloud_firestore_web (from `.symlinks/plugins/cloud_firestore_web/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_auth_web (from `.symlinks/plugins/firebase_auth_web/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_core_web (from `.symlinks/plugins/firebase_core_web/ios`)
- Flutter (from `Flutter`)
- google_sign_in (from `.symlinks/plugins/google_sign_in/ios`)
- google_sign_in_web (from `.symlinks/plugins/google_sign_in_web/ios`)
SPEC REPOS:
trunk:
- abseil
- AppAuth
- BoringSSL-GRPC
- Firebase
- FirebaseAnalytics
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseCoreDiagnosticsInterop
- FirebaseFirestore
- FirebaseInstallations
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleDataTransportCCTSupport
- GoogleSignIn
- GoogleUtilities
- "gRPC-C++"
- gRPC-Core
- GTMAppAuth
- GTMSessionFetcher
- leveldb-library
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
cloud_firestore:
:path: ".symlinks/plugins/cloud_firestore/ios"
cloud_firestore_web:
:path: ".symlinks/plugins/cloud_firestore_web/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_auth_web:
:path: ".symlinks/plugins/firebase_auth_web/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_core_web:
:path: ".symlinks/plugins/firebase_core_web/ios"
Flutter:
:path: Flutter
google_sign_in:
:path: ".symlinks/plugins/google_sign_in/ios"
google_sign_in_web:
:path: ".symlinks/plugins/google_sign_in_web/ios"
SPEC CHECKSUMS:
abseil: 18063d773f5366ff8736a050fe035a28f635fd27
AppAuth: 73574f3013a1e65b9601a3ddc8b3158cce68c09d
BoringSSL-GRPC: db8764df3204ccea016e1c8dd15d9a9ad63ff318
cloud_firestore: 31454d48df21f3e1a900015e36143c0d46a304b7
cloud_firestore_web: 9ec3dc7f5f98de5129339802d491c1204462bfec
Firebase: 0490eca762a72e4f1582319539153897f1508dee
firebase_auth: 4ee3a54d3f09434c508c284a62f895a741a30637
firebase_auth_web: 0955c07bcc06e84af76b9d4e32e6f31518f2d7de
firebase_core: 0d8be0e0d14c4902953aeb5ac5d7316d1fe4b978
firebase_core_web: d501d8b946b60c8af265428ce483b0fff5ad52d1
FirebaseAnalytics: 058d71e714a1a6804d9e0f25e3bb18e377a51579
FirebaseAuth: 5ce2b03a3d7fe56b7a6e4c5ec7ff1522890b1d6f
FirebaseAuthInterop: 0ffa57668be100582bb7643d4fcb7615496c41fc
FirebaseCore: 78276943ad85e616dfa54dafa6c89512987d9d60
FirebaseCoreDiagnostics: 2109d10c35e8289b1ee6cabf44d9ffb055620194
FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850
FirebaseFirestore: a23d596ae3a8c13d3b8353b565d2adfb690f9032
FirebaseInstallations: 575cd32f2aec0feeb0e44f5d0110a09e5e60b47b
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
google_sign_in: f32920a589fdf4ab2918ec6dc5e5b0d5b8040ff5
google_sign_in_web: 52deb24929ac0992baff65c57956031c44ed44c3
GoogleAppMeasurement: 39ecba10918b21c83877d392246157f65db351cf
GoogleDataTransport: 653963cf5be60fb59cf051e070f0836fdc305f81
GoogleDataTransportCCTSupport: 84e4d4bbab642f2e9d83ee65d78aca2b5527d314
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
GoogleUtilities: 06eb53bb579efe7099152735900dd04bf09e7275
"gRPC-C++": 9dfe7b44821e7b3e44aacad2af29d2c21f7cde83
gRPC-Core: c9aef9a261a1247e881b18059b84d597293c9947
GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
GTMSessionFetcher: cea130bbfe5a7edc8d06d3f0d17288c32ffe9925
leveldb-library: 55d93ee664b4007aac644a782d11da33fba316f7
nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd
PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6
PODFILE CHECKSUM: c34e2287a9ccaa606aeceab922830efb9a6ff69a
COCOAPODS: 1.9.1

@ -8,12 +8,9 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
385270C76FB0F533A7165A2E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BDA6743063ECE1AA5E480E /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -26,8 +23,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -35,21 +30,23 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
08134052407BF94155A97FD0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
46DCF2E0FFA915CFF3790E62 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
64BDA6743063ECE1AA5E480E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E61020DFA7983C4F990D457D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -57,8 +54,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
385270C76FB0F533A7165A2E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -68,9 +64,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
@ -84,6 +78,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
C968A41427A6C202DE27F5B1 /* Pods */,
D15429FA0FA3908CDDF0F16E /* Frameworks */,
);
sourceTree = "<group>";
};
@ -118,6 +114,25 @@
name = "Supporting Files";
sourceTree = "<group>";
};
C968A41427A6C202DE27F5B1 /* Pods */ = {
isa = PBXGroup;
children = (
E61020DFA7983C4F990D457D /* Pods-Runner.debug.xcconfig */,
46DCF2E0FFA915CFF3790E62 /* Pods-Runner.release.xcconfig */,
08134052407BF94155A97FD0 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D15429FA0FA3908CDDF0F16E /* Frameworks */ = {
isa = PBXGroup;
children = (
64BDA6743063ECE1AA5E480E /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -125,12 +140,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
AC382224AC8D58F121CB73F0 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
7028144C5268179DEEEA28F6 /* [CP] Embed Pods Frameworks */,
98E026760D1C31FD88E5C2A0 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -201,7 +219,47 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
7028144C5268179DEEEA28F6 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/AppAuth/AppAuth.framework",
"${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC/openssl_grpc.framework",
"${PODS_ROOT}/../Flutter/Flutter.framework",
"${BUILT_PRODUCTS_DIR}/GTMAppAuth/GTMAppAuth.framework",
"${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
"${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/abseil/absl.framework",
"${BUILT_PRODUCTS_DIR}/gRPC-C++/grpcpp.framework",
"${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
"${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppAuth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMAppAuth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/absl.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpcpp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@ -217,6 +275,46 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
98E026760D1C31FD88E5C2A0 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
AC382224AC8D58F121CB73F0 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

@ -6,4 +6,6 @@ import 'package:flutter/material.dart';
import 'src/app.dart';
void main() => runApp(DashboardApp());
void main() {
runApp(DashboardApp());
}

@ -0,0 +1,11 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'src/app.dart';
void main() {
runApp(DashboardApp.mock());
}

@ -2,44 +2,106 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:json_annotation/json_annotation.dart';
part 'api.g.dart';
/// Manipulates app data,
abstract class DashboardApi {
ItemApi get items;
CategoryApi get categories;
EntryApi get entries;
}
/// Manipulates [Item] data.
abstract class ItemApi {
Future<Item> delete(String id);
Future<Item> get(String id);
Future<Item> insert(Item item);
Future<List<Item>> list();
Future<Item> update(Item item, String id);
Stream<List<Item>> allItemsStream();
}
/// Manipulates [Category] data.
abstract class CategoryApi {
Future<Category> delete(String id);
/// Something being tracked.
class Item {
final String name;
String id;
Future<Category> get(String id);
Future<Category> insert(Category category);
Future<List<Category>> list();
Future<Category> update(Category category, String id);
Item(this.name);
Stream<List<Category>> subscribe();
}
/// Manipulates [Entry] data.
abstract class EntryApi {
Future<Entry> delete(String itemId, String id);
Future<Entry> insert(String itemId, Entry entry);
Future<List<Entry>> list(String itemId);
Future<Entry> update(String itemId, String id, Entry entry);
Stream<List<Entry>> allEntriesStream(String itemId);
Future<Entry> delete(String categoryId, String id);
Future<Entry> get(String categoryId, String id);
Future<Entry> insert(String categoryId, Entry entry);
Future<List<Entry>> list(String categoryId);
Future<Entry> update(String categoryId, String id, Entry entry);
Stream<List<Entry>> subscribe(String categoryId);
}
/// Something that's being tracked, e.g. Hours Slept, Cups of water, etc.
@JsonSerializable()
class Category {
String name;
@JsonKey(ignore: true)
String id;
Category(this.name);
factory Category.fromJson(Map<String, dynamic> json) =>
_$CategoryFromJson(json);
Map<String, dynamic> toJson() => _$CategoryToJson(this);
@override
operator ==(Object other) => other is Category && other.id == id;
@override
int get hashCode => id.hashCode;
@override
String toString() {
return '<Category id=$id>';
}
}
/// A number tracked at a point in time.
@JsonSerializable()
class Entry {
final int value;
final DateTime time;
int value;
@JsonKey(fromJson: _timestampToDateTime, toJson: _dateTimeToTimestamp)
DateTime time;
@JsonKey(ignore: true)
String id;
Entry(this.value, this.time);
factory Entry.fromJson(Map<String, dynamic> json) => _$EntryFromJson(json);
Map<String, dynamic> toJson() => _$EntryToJson(this);
static DateTime _timestampToDateTime(Timestamp timestamp) {
return DateTime.fromMillisecondsSinceEpoch(
timestamp.millisecondsSinceEpoch);
}
static Timestamp _dateTimeToTimestamp(DateTime dateTime) {
return Timestamp.fromMillisecondsSinceEpoch(
dateTime.millisecondsSinceEpoch);
}
@override
operator ==(Object other) => other is Entry && other.id == id;
@override
int get hashCode => id.hashCode;
@override
String toString() {
return '<Entry id=$id>';
}
}

@ -0,0 +1,33 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Category _$CategoryFromJson(Map<String, dynamic> json) {
return Category(
json['name'] as String,
);
}
Map<String, dynamic> _$CategoryToJson(Category instance) => <String, dynamic>{
'name': instance.name,
};
Entry _$EntryFromJson(Map<String, dynamic> json) {
return Entry(
json['value'] as int,
Entry._timestampToDateTime(json['time'] as Timestamp),
);
}
Map<String, dynamic> _$EntryToJson(Entry instance) => <String, dynamic>{
'value': instance.value,
'time': Entry._dateTimeToTimestamp(instance.time),
};

@ -1,3 +1,151 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'api.dart';
class FirebaseDashboardApi implements DashboardApi {
@override
final EntryApi entries;
@override
final CategoryApi categories;
FirebaseDashboardApi(Firestore firestore, String userId)
: entries = FirebaseEntryApi(firestore, userId),
categories = FirebaseCategoryApi(firestore, userId);
}
class FirebaseEntryApi implements EntryApi {
final Firestore firestore;
final String userId;
final CollectionReference _categoriesRef;
FirebaseEntryApi(this.firestore, this.userId)
: _categoriesRef = firestore.collection('users/$userId/categories');
@override
Stream<List<Entry>> subscribe(String categoryId) {
var snapshots = _categoriesRef
.document('$categoryId')
.collection('entries')
.snapshots();
var result = snapshots.map((querySnapshot) {
return querySnapshot.documents.map((snapshot) {
return Entry.fromJson(snapshot.data)..id = snapshot.documentID;
}).toList();
});
return result;
}
@override
Future<Entry> delete(String categoryId, String id) async {
var document = _categoriesRef.document('$categoryId/entries/$id');
var entry = await get(categoryId, document.documentID);
await document.delete();
return entry;
}
@override
Future<Entry> insert(String categoryId, Entry entry) async {
var document = await _categoriesRef
.document('$categoryId')
.collection('entries')
.add(entry.toJson());
return await get(categoryId, document.documentID);
}
@override
Future<List<Entry>> list(String categoryId) async {
var entriesRef =
_categoriesRef.document('$categoryId').collection('entries');
var querySnapshot = await entriesRef.getDocuments();
var entries = querySnapshot.documents
.map((doc) => Entry.fromJson(doc.data)..id = doc.documentID)
.toList();
return entries;
}
@override
Future<Entry> update(String categoryId, String id, Entry entry) async {
var document = _categoriesRef.document('$categoryId/entries/$id');
await document.setData(entry.toJson());
var snapshot = await document.get();
return Entry.fromJson(snapshot.data)..id = snapshot.documentID;
}
@override
Future<Entry> get(String categoryId, String id) async {
var document = _categoriesRef.document('$categoryId/entries/$id');
var snapshot = await document.get();
return Entry.fromJson(snapshot.data)..id = snapshot.documentID;
}
}
class FirebaseCategoryApi implements CategoryApi {
final Firestore firestore;
final String userId;
final CollectionReference _categoriesRef;
FirebaseCategoryApi(this.firestore, this.userId)
: _categoriesRef = firestore.collection('users/$userId/categories');
@override
Stream<List<Category>> subscribe() {
var snapshots = _categoriesRef.snapshots();
var result = snapshots.map((querySnapshot) {
return querySnapshot.documents.map((snapshot) {
return Category.fromJson(snapshot.data)..id = snapshot.documentID;
}).toList();
});
return result;
}
@override
Future<Category> delete(String id) async {
var document = _categoriesRef.document('$id');
var categories = await get(document.documentID);
await document.delete();
return categories;
}
@override
Future<Category> get(String id) async {
var document = _categoriesRef.document('$id');
var snapshot = await document.get();
return Category.fromJson(snapshot.data)..id = snapshot.documentID;
}
@override
Future<Category> insert(Category category) async {
var document = await _categoriesRef.add(category.toJson());
return await get(document.documentID);
}
@override
Future<List<Category>> list() async {
var querySnapshot = await _categoriesRef.getDocuments();
var categories = querySnapshot.documents
.map((doc) => Category.fromJson(doc.data)..id = doc.documentID)
.toList();
return categories;
}
@override
Future<Category> update(Category category, String id) async {
var document = _categoriesRef.document('$id');
await document.setData(category.toJson());
var snapshot = await document.get();
return Category.fromJson(snapshot.data)..id = snapshot.documentID;
}
}

@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:math';
import 'package:uuid/uuid.dart' as uuid;
@ -13,48 +14,67 @@ class MockDashboardApi implements DashboardApi {
final EntryApi entries = MockEntryApi();
@override
final ItemApi items = MockItemApi();
final CategoryApi categories = MockCategoryApi();
MockDashboardApi();
/// Creates a [MockDashboardApi] filled with mock data for the last 30 days.
Future<void> fillWithMockData() async {
await Future<void>.delayed(Duration(seconds: 1));
var category1 = await categories.insert(Category('Coffee (oz)'));
var category2 = await categories.insert(Category('Running (miles)'));
var category3 = await categories.insert(Category('Git Commits'));
var monthAgo = DateTime.now().subtract(Duration(days: 30));
for (var category in [category1, category2, category3]) {
for (var i = 0; i < 30; i++) {
var date = monthAgo.add(Duration(days: i));
var value = Random().nextInt(6) + 1;
await entries.insert(category.id, Entry(value, date));
}
}
}
}
class MockItemApi implements ItemApi {
Map<String, Item> _storage = {};
StreamController<List<Item>> _streamController =
StreamController<List<Item>>.broadcast();
class MockCategoryApi implements CategoryApi {
Map<String, Category> _storage = {};
StreamController<List<Category>> _streamController =
StreamController<List<Category>>.broadcast();
@override
Future<Item> delete(String id) async {
Future<Category> delete(String id) async {
var removed = _storage.remove(id);
_emit();
return _storage.remove(id);
return removed;
}
@override
Future<Item> get(String id) async {
Future<Category> get(String id) async {
return _storage[id];
}
@override
Future<Item> insert(Item item) async {
Future<Category> insert(Category category) async {
var id = uuid.Uuid().v4();
var newItem = Item(item.name)..id = id;
_storage[id] = newItem;
var newCategory = Category(category.name)..id = id;
_storage[id] = newCategory;
_emit();
return newItem;
return newCategory;
}
@override
Future<List<Item>> list() async {
Future<List<Category>> list() async {
return _storage.values.toList();
}
@override
Future<Item> update(Item item, String id) async {
_storage[id] = item;
return item..id = id;
Future<Category> update(Category category, String id) async {
_storage[id] = category;
_emit();
return category..id = id;
}
Stream<List<Item>> allItemsStream() {
return _streamController.stream;
}
Stream<List<Category>> subscribe() => _streamController.stream;
void _emit() {
_streamController.add(_storage.values.toList());
@ -63,44 +83,64 @@ class MockItemApi implements ItemApi {
class MockEntryApi implements EntryApi {
Map<String, Entry> _storage = {};
StreamController<List<Entry>> _streamController =
StreamController<List<Entry>>.broadcast();
StreamController<_EntriesEvent> _streamController =
StreamController.broadcast();
@override
Future<Entry> delete(String itemId, String id) async {
_emit();
return _storage.remove('$itemId-$id');
Future<Entry> delete(String categoryId, String id) async {
_emit(categoryId);
return _storage.remove('$categoryId-$id');
}
@override
Future<Entry> insert(String itemId, Entry entry) async {
Future<Entry> insert(String categoryId, Entry entry) async {
var id = uuid.Uuid().v4();
var newEntry = Entry(entry.value, entry.time)..id = id;
_storage['$itemId-$id'] = newEntry;
_emit();
_storage['$categoryId-$id'] = newEntry;
_emit(categoryId);
return newEntry;
}
@override
Future<List<Entry>> list(String itemId) async {
Future<List<Entry>> list(String categoryId) async {
return _storage.keys
.where((k) => k.startsWith(itemId))
.where((k) => k.startsWith(categoryId))
.map((k) => _storage[k])
.toList();
}
@override
Future<Entry> update(String itemId, String id, Entry entry) async {
_storage['$itemId-$id'] = entry;
Future<Entry> update(String categoryId, String id, Entry entry) async {
_storage['$categoryId-$id'] = entry;
_emit(categoryId);
return entry..id = id;
}
@override
Stream<List<Entry>> allEntriesStream(String itemId) {
return _streamController.stream;
Stream<List<Entry>> subscribe(String categoryId) {
return _streamController.stream
.where((event) => event.categoryId == categoryId)
.map((event) => event.entries);
}
void _emit() {
_streamController.add(_storage.values.toList());
void _emit(String categoryId) {
var entries = _storage.keys
.where((k) => k.startsWith(categoryId))
.map((k) => _storage[k])
.toList();
_streamController.add(_EntriesEvent(categoryId, entries));
}
@override
Future<Entry> get(String categoryId, String id) async {
return _storage['$categoryId-$id'];
}
}
class _EntriesEvent {
final String categoryId;
final List<Entry> entries;
_EntriesEvent(this.categoryId, this.entries);
}

@ -2,58 +2,102 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'api/api.dart';
import 'api/firebase.dart';
import 'api/mock.dart';
import 'auth/auth.dart';
import 'auth/firebase.dart';
import 'auth/mock.dart';
import 'pages/home.dart';
import 'widgets/third_party/adaptive_scaffold.dart';
import 'pages/sign_in.dart';
/// An app that shows a responsive dashboard.
/// The global state the app.
class AppState {
final Auth auth;
DashboardApi api;
AppState(this.auth);
}
/// Creates a [DashboardApi] when the user is logged in.
typedef DashboardApi ApiBuilder(User user);
/// An app that displays a personalized dashboard.
class DashboardApp extends StatefulWidget {
static ApiBuilder _mockApiBuilder =
(user) => MockDashboardApi()..fillWithMockData();
static ApiBuilder _apiBuilder =
(user) => FirebaseDashboardApi(Firestore.instance, user.uid);
final Auth auth;
final ApiBuilder apiBuilder;
/// Runs the app using Firebase
DashboardApp()
: auth = FirebaseAuthService(),
apiBuilder = _apiBuilder;
/// Runs the app using mock data
DashboardApp.mock()
: auth = MockAuthService(),
apiBuilder = _mockApiBuilder;
@override
_DashboardAppState createState() => _DashboardAppState();
}
class _DashboardAppState extends State<DashboardApp> {
int _pageIndex = 0;
AppState _appState;
void initState() {
super.initState();
_appState = AppState(widget.auth);
}
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<DashboardApi>(create: (_) => MockDashboardApi()),
],
return Provider.value(
value: _appState,
child: MaterialApp(
home: AdaptiveScaffold(
currentIndex: _pageIndex,
destinations: [
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
AdaptiveScaffoldDestination(title: 'Entries', icon: Icons.list),
AdaptiveScaffoldDestination(
title: 'Settings', icon: Icons.settings),
],
body: _pageAtIndex(_pageIndex),
onNavigationIndexChange: (newIndex) {
setState(() {
_pageIndex = newIndex;
});
},
home: Builder(
builder: (context) => SignInPage(
auth: _appState.auth,
onSuccess: (user) => _handleSignIn(user, context, _appState),
),
),
),
);
}
static Widget _pageAtIndex(int index) {
switch (index) {
case 1:
return Center(child: Text('page 2'));
case 2:
return Center(child: Text('page 3'));
case 0:
default:
return HomePage();
}
/// Sets the DashboardApi on AppState and navigates to the home page.
void _handleSignIn(User user, BuildContext context, AppState appState) {
appState.api = widget.apiBuilder(user);
_showPage(HomePage(), context);
}
/// Navigates to the home page using a fade transition.
void _showPage(Widget page, BuildContext context) {
var route = _fadeRoute(page);
Navigator.of(context).pushReplacement(route);
}
/// Creates a [Route] that shows [newPage] using a fade transition.
Route<FadeTransition> _fadeRoute(Widget newPage) {
return PageRouteBuilder<FadeTransition>(
pageBuilder: (context, animation, secondaryAnimation) {
return newPage;
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation.drive(CurveTween(curve: Curves.ease)),
child: child,
);
},
);
}
}

@ -0,0 +1,12 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
abstract class Auth {
Future<User> signIn();
Future signOut();
}
abstract class User {
String get uid;
}

@ -0,0 +1,41 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart' hide FirebaseUser;
import 'auth.dart';
class FirebaseAuthService implements Auth {
final GoogleSignIn _googleSignIn = GoogleSignIn();
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<User> signIn() async {
GoogleSignInAccount googleUser;
if (await _googleSignIn.isSignedIn()) {
googleUser = await _googleSignIn.signInSilently();
} else {
googleUser = await _googleSignIn.signIn();
}
var googleAuth = await googleUser.authentication;
var credential = GoogleAuthProvider.getCredential(
accessToken: googleAuth.accessToken, idToken: googleAuth.idToken);
var authResult = await _auth.signInWithCredential(credential);
return _FirebaseUser(authResult.user.uid);
}
Future<void> signOut() async {
await _auth.signOut();
}
}
class _FirebaseUser implements User {
final String uid;
_FirebaseUser(this.uid);
}

@ -0,0 +1,21 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'auth.dart';
class MockAuthService implements Auth {
@override
Future<User> signIn() async {
return MockUser();
}
@override
Future signOut() async {
return null;
}
}
class MockUser implements User {
String get uid => "123";
}

@ -0,0 +1,67 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../api/api.dart';
import '../app.dart';
import '../widgets/category_chart.dart';
class DashboardPage extends StatelessWidget {
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return FutureBuilder<List<Category>>(
future: appState.api.categories.list(),
builder: (context, futureSnapshot) {
if (!futureSnapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return StreamBuilder<List<Category>>(
initialData: futureSnapshot.data,
stream: appState.api.categories.subscribe(),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return Dashboard(snapshot.data);
},
);
},
);
}
}
class Dashboard extends StatelessWidget {
final List<Category> categories;
Dashboard(this.categories);
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return Scrollbar(
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
childAspectRatio: 2,
maxCrossAxisExtent: 500,
),
children: [
...categories.map(
(category) => Card(
child: CategoryChart(
category: category,
api: api,
),
),
)
],
),
);
}
}

@ -0,0 +1,161 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart' as intl;
import '../api/api.dart';
import '../app.dart';
import '../widgets/categories_dropdown.dart';
import '../widgets/dialogs.dart';
class EntriesPage extends StatefulWidget {
@override
_EntriesPageState createState() => _EntriesPageState();
}
class _EntriesPageState extends State<EntriesPage> {
Category _selected;
@override
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return Column(
children: [
CategoryDropdown(
api: appState.api.categories,
onSelected: (category) => setState(() => _selected = category)),
Expanded(
child: _selected == null
? Center(child: CircularProgressIndicator())
: EntriesList(
category: _selected,
api: appState.api.entries,
),
),
],
);
}
}
class EntriesList extends StatefulWidget {
final Category category;
final EntryApi api;
EntriesList({
@required this.category,
@required this.api,
}) : super(key: ValueKey(category.id));
@override
_EntriesListState createState() => _EntriesListState();
}
class _EntriesListState extends State<EntriesList> {
@override
Widget build(BuildContext context) {
if (widget.category == null) {
return _buildLoadingIndicator();
}
return FutureBuilder<List<Entry>>(
future: widget.api.list(widget.category.id),
builder: (context, futureSnapshot) {
if (!futureSnapshot.hasData) {
return _buildLoadingIndicator();
}
return StreamBuilder<List<Entry>>(
initialData: futureSnapshot.data,
stream: widget.api.subscribe(widget.category.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return _buildLoadingIndicator();
}
return ListView.builder(
itemBuilder: (context, index) {
return EntryTile(
category: widget.category,
entry: snapshot.data[index],
);
},
itemCount: snapshot.data.length,
);
},
);
},
);
}
Widget _buildLoadingIndicator() {
return Center(child: CircularProgressIndicator());
}
}
class EntryTile extends StatelessWidget {
final Category category;
final Entry entry;
EntryTile({
this.category,
this.entry,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(entry.value.toString()),
subtitle: Text(intl.DateFormat('MM/dd/yy h:mm a').format(entry.time)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlatButton(
child: Text('Edit'),
onPressed: () {
showDialog<void>(
context: context,
builder: (context) {
return EditEntryDialog(category: category, entry: entry);
},
);
},
),
FlatButton(
child: Text('Delete'),
onPressed: () async {
var shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete entry?'),
actions: [
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(false),
),
FlatButton(
child: Text('Delete'),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
);
if (shouldDelete) {
await Provider.of<AppState>(context, listen: false)
.api
.entries
.delete(category.id, entry.id);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Entry deleted'),
),
);
}
},
),
],
),
);
}
}

@ -3,47 +3,79 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../api/api.dart';
import 'item_details.dart';
import '../widgets/dialogs.dart';
import '../widgets/third_party/adaptive_scaffold.dart';
import 'dashboard.dart';
import 'entries.dart';
class HomePage extends StatelessWidget {
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _pageIndex = 0;
@override
Widget build(BuildContext context) {
var api = Provider.of<DashboardApi>(context);
return Scaffold(
body: StreamProvider<List<Item>>(
initialData: [],
create: (context) => api.items.allItemsStream(),
child: Consumer<List<Item>>(
builder: (context, items, child) {
return ListView.builder(
itemBuilder: (context, idx) {
return ListTile(
title: Text(items[idx].name),
onTap: () {
_showDetails(items[idx], context);
},
);
},
itemCount: items.length,
);
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
api.items.insert(Item('Coffees Drank'));
},
),
return AdaptiveScaffold(
currentIndex: _pageIndex,
destinations: [
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
AdaptiveScaffoldDestination(title: 'Entries', icon: Icons.list),
AdaptiveScaffoldDestination(title: 'Settings', icon: Icons.settings),
],
body: _pageAtIndex(_pageIndex),
onNavigationIndexChange: (newIndex) {
setState(() {
_pageIndex = newIndex;
});
},
floatingActionButton:
_hasFloatingActionButton ? _buildFab(context) : null,
);
}
void _showDetails(Item item, BuildContext context) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return ItemDetailsPage(item);
}));
bool get _hasFloatingActionButton {
if (_pageIndex == 2) return false;
return true;
}
FloatingActionButton _buildFab(BuildContext context) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => _handleFabPressed(),
);
}
void _handleFabPressed() {
if (_pageIndex == 0) {
showDialog<NewCategoryDialog>(
context: context,
builder: (context) => NewCategoryDialog(),
);
return;
}
if (_pageIndex == 1) {
showDialog<NewEntryDialog>(
context: context,
builder: (context) => NewEntryDialog(),
);
return;
}
}
Widget _pageAtIndex(int index) {
if (index == 0) {
return DashboardPage();
}
if (index == 1) {
return EntriesPage();
}
return Center(child: Text('Settings page'));
}
}

@ -1,22 +0,0 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:web_dashboard/src/api/api.dart';
class ItemDetailsPage extends StatelessWidget {
final Item item;
ItemDetailsPage(this.item);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('${item.name}'),
),
);
}
}

@ -0,0 +1,46 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import '../auth/auth.dart';
class SignInPage extends StatefulWidget {
final Auth auth;
final ValueChanged<User> onSuccess;
SignInPage({
@required this.auth,
@required this.onSuccess,
});
@override
_SignInPageState createState() => _SignInPageState();
}
class _SignInPageState extends State<SignInPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Sign In'),
onPressed: () async {
var user = await widget.auth.signIn();
if (user != null) {
widget.onSuccess(user);
} else {
throw ('Unable to sign in');
}
},
),
),
);
}
}

@ -0,0 +1,65 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import '../api/api.dart';
import 'day_helpers.dart';
/// The total value of one or more [Entry]s on a given day.
class EntryTotal {
final DateTime day;
int value;
EntryTotal(this.day, this.value);
}
/// Returns a list of [EntryTotal] objects. Each [EntryTotal] is the sum of
/// the values of all the entries on a given day.
List<EntryTotal> entryTotalsByDay(List<Entry> entries, int daysAgo,
{DateTime today}) {
today ??= DateTime.now();
return _entryTotalsByDay(entries, daysAgo, today).toList();
}
Iterable<EntryTotal> _entryTotalsByDay(
List<Entry> entries, int daysAgo, DateTime today) sync* {
var start = today.subtract(Duration(days: daysAgo));
var entriesByDay = _entriesInRange(start, today, entries);
for (var i = 0; i < entriesByDay.length; i++) {
var list = entriesByDay[i];
var entryTotal = EntryTotal(start.add(Duration(days: i)), 0);
for (var entry in list) {
entryTotal.value += entry.value;
}
yield entryTotal;
}
}
/// Groups entries by day between [start] and [end]. The result is a list of
/// lists. The outer list represents the number of days since [start], and the
/// inner list is the group of entries on that day.
List<List<Entry>> _entriesInRange(
DateTime start, DateTime end, List<Entry> entries) =>
_entriesInRangeImpl(start, end, entries).toList();
Iterable<List<Entry>> _entriesInRangeImpl(
DateTime start, DateTime end, List<Entry> entries) sync* {
start = start.atMidnight;
end = end.atMidnight;
var d = start;
while (d.compareTo(end) <= 0) {
var es = <Entry>[];
for (var entry in entries) {
if (d.isSameDay(entry.time.atMidnight)) {
es.add(entry);
}
}
yield es;
d = d.add(Duration(days: 1));
}
}

@ -0,0 +1,15 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
extension DayUtils on DateTime {
/// The UTC date portion of a datetime, without the minutes, seconds, etc.
DateTime get atMidnight {
return DateTime.utc(year, month, day);
}
/// Checks that the two [DateTime]s share the same date.
bool isSameDay(DateTime d2) {
return this.year == d2.year && this.month == d2.month && this.day == d2.day;
}
}

@ -0,0 +1,108 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import '../api/api.dart';
/// Subscribes to the latest list of categories and allows the user to select
/// one.
class CategoryDropdown extends StatefulWidget {
final CategoryApi api;
final ValueChanged<Category> onSelected;
CategoryDropdown({
@required this.api,
@required this.onSelected,
});
@override
_CategoryDropdownState createState() => _CategoryDropdownState();
}
class _CategoryDropdownState extends State<CategoryDropdown> {
Category _selected;
Future<List<Category>> _future;
Stream<List<Category>> _stream;
void initState() {
super.initState();
// This widget needs to wait for the list of Categories, select the first
// Category, and emit an `onSelected` event.
//
// This could be done inside the FutureBuilder's `builder` callback,
// but calling setState() during the build is an error. (Calling the
// onSelected callback will also cause the parent widget to call
// setState()).
//
// Instead, we'll create a new Future that sets the selected Category and
// calls `onSelected` if necessary. Then, we'll pass *that* future to
// FutureBuilder. Now the selected category is set and events are emitted
// *before* the build is triggered by the FutureBuilder.
_future = widget.api.list().then((categories) {
if (categories.isEmpty) {
return categories;
}
_setSelected(categories.first);
return categories;
});
// Same here, we'll create a new stream that handles any potential
// setState() operations before we trigger our StreamBuilder.
_stream = widget.api.subscribe().map((categories) {
if (!categories.contains(_selected) && categories.isNotEmpty) {
_setSelected(categories.first);
}
return categories;
});
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Category>>(
future: _future,
builder: (context, futureSnapshot) {
// Show an empty dropdown while the data is loading.
if (!futureSnapshot.hasData) {
return DropdownButton<Category>(items: [], onChanged: null);
}
return StreamBuilder<List<Category>>(
initialData: futureSnapshot.hasData ? futureSnapshot.data : [],
stream: _stream,
builder: (context, snapshot) {
var data = snapshot.hasData ? snapshot.data : <Category>[];
return DropdownButton<Category>(
value: _selected,
items: data.map(_buildDropdownItem).toList(),
onChanged: (category) {
_setSelected(category);
},
);
},
);
},
);
}
void _setSelected(Category category) {
if (_selected == category) {
return;
}
setState(() {
_selected = category;
});
widget.onSelected(_selected);
}
DropdownMenuItem<Category> _buildDropdownItem(Category category) {
return DropdownMenuItem<Category>(
child: Text(category.name), value: category);
}
}

@ -0,0 +1,111 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:intl/intl.dart' as intl;
import '../api/api.dart';
import '../utils/chart_utils.dart' as utils;
import 'dialogs.dart';
// The number of days to show in the chart
const _daysBefore = 10;
class CategoryChart extends StatelessWidget {
final Category category;
final DashboardApi api;
CategoryChart({
@required this.category,
@required this.api,
});
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(category.name),
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
showDialog<EditCategoryDialog>(
context: context,
builder: (context) {
return EditCategoryDialog(category: category);
},
);
},
),
],
),
),
Expanded(
// Load the initial snapshot using a FutureBuilder, and subscribe to
// additional updates with a StreamBuilder.
child: FutureBuilder<List<Entry>>(
future: api.entries.list(category.id),
builder: (context, futureSnapshot) {
if (!futureSnapshot.hasData) {
return _buildLoadingIndicator();
}
return StreamBuilder<List<Entry>>(
initialData: futureSnapshot.data,
stream: api.entries.subscribe(category.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return _buildLoadingIndicator();
}
return _BarChart(entries: snapshot.data);
},
);
},
),
),
],
);
}
Widget _buildLoadingIndicator() {
return Center(child: CircularProgressIndicator());
}
}
class _BarChart extends StatelessWidget {
final List<Entry> entries;
_BarChart({this.entries});
@override
Widget build(BuildContext context) {
return charts.BarChart(
[_seriesData()],
animate: false,
);
}
charts.Series<utils.EntryTotal, String> _seriesData() {
return charts.Series<utils.EntryTotal, String>(
id: 'Entries',
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
domainFn: (entryTotal, _) {
if (entryTotal == null) return null;
var format = intl.DateFormat.Md();
return format.format(entryTotal.day);
},
measureFn: (total, _) {
if (total == null) return null;
return total.value;
},
data: utils.entryTotalsByDay(entries, _daysBefore),
);
}
}

@ -0,0 +1,103 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:web_dashboard/src/app.dart';
class NewCategoryForm extends StatefulWidget {
@override
_NewCategoryFormState createState() => _NewCategoryFormState();
}
class _NewCategoryFormState extends State<NewCategoryForm> {
Category _category = Category('');
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return EditCategoryForm(
category: _category,
onDone: (shouldInsert) {
if (shouldInsert) {
api.categories.insert(_category);
}
Navigator.of(context).pop();
},
);
}
}
class EditCategoryForm extends StatefulWidget {
final Category category;
final ValueChanged<bool> onDone;
EditCategoryForm({
@required this.category,
@required this.onDone,
});
@override
_EditCategoryFormState createState() => _EditCategoryFormState();
}
class _EditCategoryFormState extends State<EditCategoryForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
initialValue: widget.category.name,
decoration: InputDecoration(
labelText: 'Name',
),
onChanged: (newValue) {
widget.category.name = newValue;
},
validator: (value) {
if (value.isEmpty) {
return 'Please enter a name';
}
return null;
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
widget.onDone(false);
},
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('OK'),
onPressed: () {
if (_formKey.currentState.validate()) {
widget.onDone(true);
}
},
),
),
],
),
],
),
);
}
}

@ -0,0 +1,98 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:web_dashboard/src/widgets/category_forms.dart';
import '../app.dart';
import 'edit_entry.dart';
class NewCategoryDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text('New Category'),
children: <Widget>[
NewCategoryForm(),
],
);
}
}
class EditCategoryDialog extends StatelessWidget {
final Category category;
EditCategoryDialog({
@required this.category,
});
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return SimpleDialog(
title: Text('Edit Category'),
children: [
EditCategoryForm(
category: category,
onDone: (shouldUpdate) {
if (shouldUpdate) {
api.categories.update(category, category.id);
}
Navigator.of(context).pop();
},
),
],
);
}
}
class NewEntryDialog extends StatefulWidget {
@override
_NewEntryDialogState createState() => _NewEntryDialogState();
}
class _NewEntryDialogState extends State<NewEntryDialog> {
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text('New Entry'),
children: [
NewEntryForm(),
],
);
}
}
class EditEntryDialog extends StatelessWidget {
final Category category;
final Entry entry;
EditEntryDialog({
this.category,
this.entry,
});
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return SimpleDialog(
title: Text('Edit Entry'),
children: [
EditEntryForm(
entry: entry,
onDone: (shouldUpdate) {
if (shouldUpdate) {
api.entries.update(category.id, entry.id, entry);
}
Navigator.of(context).pop();
},
)
],
);
}
}

@ -0,0 +1,154 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:intl/intl.dart' as intl;
import '../app.dart';
import 'categories_dropdown.dart';
class NewEntryForm extends StatefulWidget {
@override
_NewEntryFormState createState() => _NewEntryFormState();
}
class _NewEntryFormState extends State<NewEntryForm> {
Category _selected;
Entry _entry = Entry(0, DateTime.now());
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: CategoryDropdown(
api: api.categories,
onSelected: (category) {
setState(() {
_selected = category;
});
},
),
),
EditEntryForm(
entry: _entry,
onDone: (shouldInsert) {
if (shouldInsert) {
api.entries.insert(_selected.id, _entry);
}
Navigator.of(context).pop();
},
),
],
);
}
}
class EditEntryForm extends StatefulWidget {
final Entry entry;
final ValueChanged<bool> onDone;
EditEntryForm({
@required this.entry,
@required this.onDone,
});
@override
_EditEntryFormState createState() => _EditEntryFormState();
}
class _EditEntryFormState extends State<EditEntryForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
initialValue: widget.entry.value.toString(),
decoration: InputDecoration(labelText: 'Value'),
keyboardType: TextInputType.number,
validator: (value) {
try {
int.parse(value);
} catch (e) {
return "Please enter a whole number";
}
return null;
},
onChanged: (newValue) {
try {
widget.entry.value = int.parse(newValue);
} on FormatException {
print('Entry cannot contain "$newValue". Expected a number');
}
},
),
),
Padding(
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(intl.DateFormat('MM/dd/yyyy').format(widget.entry.time)),
RaisedButton(
child: Text('Edit'),
onPressed: () async {
var result = await showDatePicker(
context: context,
initialDate: widget.entry.time,
firstDate: DateTime.now().subtract(Duration(days: 365)),
lastDate: DateTime.now());
if (result == null) {
return;
}
setState(() {
widget.entry.time = result;
});
},
)
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
widget.onDone(false);
},
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('OK'),
onPressed: () {
if (_formKey.currentState.validate()) {
widget.onDone(true);
}
},
),
),
],
)
],
),
);
}
}

@ -7,21 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "2.1.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.8"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
version: "0.39.6"
args:
dependency: transitive
description:
@ -43,6 +36,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.2"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "7.0.9"
charcode:
dependency: transitive
description:
@ -50,6 +99,69 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
charts_common:
dependency: transitive
description:
name: charts_common
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0"
charts_flutter:
dependency: "direct main"
description:
name: charts_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3+2"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
cloud_firestore:
dependency: "direct main"
description:
name: cloud_firestore
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.4+2"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+2"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
collection:
dependency: transitive
description:
@ -92,6 +204,76 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.4"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
firebase:
dependency: transitive
description:
name: firebase
url: "https://pub.dartlang.org"
source: hosted
version: "7.3.0"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.5+3"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.4+3"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+2"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.11"
flutter:
dependency: "direct main"
description: flutter
@ -102,6 +284,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
@ -109,6 +296,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
google_sign_in:
dependency: "direct main"
description:
name: google_sign_in
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.1"
google_sign_in_platform_interface:
dependency: transitive
description:
name: google_sign_in_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
google_sign_in_web:
dependency: transitive
description:
name: google_sign_in_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4"
graphs:
dependency: transitive
description:
name: graphs
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
grinder:
dependency: "direct dev"
description:
name: grinder
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4"
html:
dependency: transitive
description:
@ -122,7 +344,7 @@ packages:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.1"
version: "0.12.0+4"
http_multi_server:
dependency: transitive
description:
@ -137,13 +359,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
image:
intl:
dependency: transitive
description:
name: image
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
version: "0.16.1"
io:
dependency: transitive
description:
@ -158,6 +380,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
logging:
dependency: transitive
description:
@ -213,7 +449,7 @@ packages:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.0.1+2"
node_preamble:
dependency: transitive
description:
@ -234,7 +470,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
version: "1.7.0"
pedantic:
dependency: transitive
description:
@ -242,13 +478,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
petitparser:
plugin_platform_interface:
dependency: transitive
description:
name: petitparser
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
version: "1.0.2"
pool:
dependency: transitive
description:
@ -262,7 +498,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
version: "4.0.4"
pub_semver:
dependency: transitive
description:
@ -270,6 +506,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
quiver:
dependency: transitive
description:
@ -310,6 +553,13 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.5"
source_map_stack_trace:
dependency: transitive
description:
@ -345,6 +595,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
stream_transform:
dependency: transitive
description:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
string_scanner:
dependency: transitive
description:
@ -365,7 +622,7 @@ packages:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.3"
version: "1.14.2"
test_api:
dependency: transitive
description:
@ -379,7 +636,14 @@ packages:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
version: "0.3.3"
timing:
dependency: transitive
description:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+2"
typed_data:
dependency: transitive
description:
@ -407,14 +671,14 @@ packages:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
version: "4.0.0"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
version: "0.9.7+14"
web_socket_channel:
dependency: transitive
description:
@ -428,21 +692,14 @@ packages:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.3"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
version: "0.5.0+1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.2.0"
sdks:
dart: ">=2.7.0 <3.0.0"
flutter: ">=1.17.0"
flutter: ">=1.12.13+hotfix.4 <2.0.0"

@ -1,17 +1,26 @@
name: web_dashboard
description: A desktop-friendly dashboard app
description: A dashboard app sample
version: 1.0.0+1
environment:
sdk: ">=2.3.0 <3.0.0"
sdk: ">=2.6.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^0.13.0
cupertino_icons: ^0.1.2
firebase_auth: ^0.15.0
firebase_core: ^0.4.3
google_sign_in: ^4.4 0
json_annotation: ^3.0.0
provider: ^4.0.0
uuid: ^2.0.0
charts_flutter: ^0.9.0
dev_dependencies:
flutter_test:
sdk: flutter
test: any
build_runner: ^1.8.0
json_serializable: ^3.3.0
test: ^1.14.0
grinder: ^0.8.4
flutter:
uses-material-design: true

@ -0,0 +1,29 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:test/test.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:web_dashboard/src/utils/chart_utils.dart';
void main() {
group('chart utils', () {
test('totals entries by day', () async {
var entries = [
Entry(10, DateTime(2020, 3, 1)),
Entry(10, DateTime(2020, 3, 1)),
Entry(10, DateTime(2020, 3, 2)),
];
var totals = entryTotalsByDay(entries, 2, today: DateTime(2020, 3, 2));
expect(totals, hasLength(3));
expect(totals[1].value, 20);
expect(totals[2].value, 10);
});
test('days', () async {
expect(
DateTime.utc(2020, 1, 3).difference(DateTime.utc(2020, 1, 2)).inDays,
1);
});
});
}

@ -17,80 +17,81 @@ void main() {
group('items', () {
test('insert', () async {
var item = await api.items.insert(Item('Coffees Drank'));
expect(item.name, 'Coffees Drank');
var category = await api.categories.insert(Category('Coffees Drank'));
expect(category.name, 'Coffees Drank');
});
test('delete', () async {
await api.items.insert(Item('Coffees Drank'));
var item2 = await api.items.insert(Item('Miles Ran'));
var removed = await api.items.delete(item2.id);
await api.categories.insert(Category('Coffees Drank'));
var category = await api.categories.insert(Category('Miles Ran'));
var removed = await api.categories.delete(category.id);
expect(removed.name, 'Miles Ran');
var items = await api.items.list();
expect(items, hasLength(1));
var categories = await api.categories.list();
expect(categories, hasLength(1));
});
test('update', () async {
var item = await api.items.insert(Item('Coffees Drank'));
await api.items.update(Item('Bagels Consumed'), item.id);
var category = await api.categories.insert(Category('Coffees Drank'));
await api.categories.update(Category('Bagels Consumed'), category.id);
var latest = await api.items.get(item.id);
var latest = await api.categories.get(category.id);
expect(latest.name, equals('Bagels Consumed'));
});
test('subscribe', () async {
var stream = api.items.allItemsStream();
var stream = api.categories.subscribe();
stream.listen(expectAsync1((x) {
expect(x, hasLength(1));
expect(x.first.name, equals('Coffees Drank'));
}, count: 1));
await api.items.insert(Item('Coffees Drank'));
await api.categories.insert(Category('Coffees Drank'));
});
});
group('entry service', () {
Item item;
Category category;
DateTime dateTime = DateTime(2020, 1, 1, 30, 45);
setUp(() async {
item = await api.items.insert(Item('Lines of code committed'));
category =
await api.categories.insert(Category('Lines of code committed'));
});
test('insert', () async {
var entry = await api.entries.insert(item.id, Entry(1, dateTime));
var entry = await api.entries.insert(category.id, Entry(1, dateTime));
expect(entry.value, 1);
expect(entry.time, dateTime);
});
test('delete', () async {
await api.entries.insert(item.id, Entry(1, dateTime));
var entry2 = await api.entries.insert(item.id, Entry(2, dateTime));
await api.entries.insert(category.id, Entry(1, dateTime));
var entry2 = await api.entries.insert(category.id, Entry(2, dateTime));
await api.entries.delete(item.id, entry2.id);
await api.entries.delete(category.id, entry2.id);
var entries = await api.entries.list(item.id);
var entries = await api.entries.list(category.id);
expect(entries, hasLength(1));
});
test('update', () async {
var entry = await api.entries.insert(item.id, Entry(1, dateTime));
var entry = await api.entries.insert(category.id, Entry(1, dateTime));
var updated =
await api.entries.update(item.id, entry.id, Entry(2, dateTime));
await api.entries.update(category.id, entry.id, Entry(2, dateTime));
expect(updated.value, 2);
});
test('subscribe', () async {
var stream = api.entries.allEntriesStream(item.id);
var stream = api.entries.subscribe(category.id);
stream.listen(expectAsync1((x) {
expect(x, hasLength(1));
expect(x.first.value, equals(1));
}, count: 1));
api.entries.insert(item.id, Entry(1, dateTime));
await api.entries.insert(category.id, Entry(1, dateTime));
});
});
});

@ -0,0 +1,108 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:grinder/grinder.dart';
void main(List<String> args) => grind(args);
@Task()
void runSkia() {
run('flutter',
arguments:
'run -d web --web-port=5000 --release --dart-define=FLUTTER_WEB_USE_SKIA=true lib/main.dart '
.split(' '));
}
@Task()
void runWeb() {
run('flutter',
arguments: 'run -d web --web-port=5000 lib/main.dart '.split(' '));
}
@Task()
void runMock() {
run('flutter',
arguments: 'run -d web --web-port=5000 lib/main_mock.dart '.split(' '));
}
@Task()
void runMockSkia() {
run('flutter',
arguments:
'run -d web --web-port=5000 --release --dart-define=FLUTTER_WEB_USE_SKIA=true lib/main_mock.dart'
.split(' '));
}
@Task()
void test() {
TestRunner().testAsync();
}
@DefaultTask()
@Depends(test, copyright)
void build() {
Pub.build();
}
@Task()
void clean() => defaultClean();
@Task()
void generate() {
Pub.run('build_runner', arguments: ['build']);
}
const _copyright =
'''// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.''';
@Task()
Future copyright() async {
var files = <File>[];
await for (var file in _filesWithoutCopyright()) {
files.add(file);
}
if (files.isNotEmpty) {
log('Found Dart files without a copyright header:');
for (var file in files) {
log(file.toString());
}
fail('run "grind fix-copyright" to add copyright headers');
}
}
@Task()
Future fixCopyright() async {
await for (var file in _filesWithoutCopyright()) {
var contents = await file.readAsString();
await file.writeAsString(_copyright + '\n\n' + contents);
}
}
Stream<File> _filesWithoutCopyright() async* {
var set = FileSet.fromDir(Directory('.'), recurse: true);
var dartFiles =
set.files.where((file) => path.extension(file.path) == '.dart');
for (var file in dartFiles) {
var firstThreeLines = await file
.openRead()
.transform(utf8.decoder)
.transform(LineSplitter())
.take(3)
.fold<String>('', (previous, element) {
if (previous == '') return element;
return previous + '\n' + element;
});
if (firstThreeLines != _copyright) {
yield file;
}
}
}

@ -0,0 +1,12 @@
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "<YOUR_PROJECT_ID>",
storageBucket: "",
messagingSenderId: "",
appId: ""
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

@ -13,6 +13,15 @@
<title>web_dashboard</title>
<link rel="manifest" href="/manifest.json">
<!-- Firebase Setup -->
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-firestore.js"></script>
<script src="firebase_init.js"></script>
<!-- Uncomment and add Firebase client ID here: -->
<!-- <meta name="google-signin-client_id" content="<YOUR WEB CLIENT ID>"> -->
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to

@ -0,0 +1,11 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Empty test', (tester) async {
expect(1, 1);
});
}
Loading…
Cancel
Save