mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 22:09:06 +00:00
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
This commit is contained in:
@@ -30,3 +30,94 @@ Skia / CanvasKit mode:
|
|||||||
flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true
|
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
|
||||||
|
|||||||
31
experimental/web_dashboard/analysis_options.yaml
Normal file
31
experimental/web_dashboard/analysis_options.yaml
Normal file
@@ -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 {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "dev.flutter.web_dashboard"
|
applicationId "dev.flutter.web_dashboard"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
87
experimental/web_dashboard/ios/Podfile
Normal file
87
experimental/web_dashboard/ios/Podfile
Normal file
@@ -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
|
||||||
410
experimental/web_dashboard/ios/Podfile.lock
Normal file
410
experimental/web_dashboard/ios/Podfile.lock
Normal file
@@ -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 */
|
/* Begin PBXBuildFile section */
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
@@ -26,8 +23,6 @@
|
|||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
|
|
||||||
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
|
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -35,21 +30,23 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -57,8 +54,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
|
385270C76FB0F533A7165A2E /* Pods_Runner.framework in Frameworks */,
|
||||||
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -68,9 +64,7 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3B80C3931E831B6300D905FE /* App.framework */,
|
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEBA1CF902C7004384FC /* Flutter.framework */,
|
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
@@ -84,6 +78,8 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */,
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
C968A41427A6C202DE27F5B1 /* Pods */,
|
||||||
|
D15429FA0FA3908CDDF0F16E /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -118,6 +114,25 @@
|
|||||||
name = "Supporting Files";
|
name = "Supporting Files";
|
||||||
sourceTree = "<group>";
|
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 */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -125,12 +140,15 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
AC382224AC8D58F121CB73F0 /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
7028144C5268179DEEEA28F6 /* [CP] Embed Pods Frameworks */,
|
||||||
|
98E026760D1C31FD88E5C2A0 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -201,7 +219,47 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
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 */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
@@ -217,6 +275,46 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|||||||
@@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'src/app.dart';
|
import 'src/app.dart';
|
||||||
|
|
||||||
void main() => runApp(DashboardApp());
|
void main() {
|
||||||
|
runApp(DashboardApp());
|
||||||
|
}
|
||||||
|
|||||||
11
experimental/web_dashboard/lib/main_mock.dart
Normal file
11
experimental/web_dashboard/lib/main_mock.dart
Normal file
@@ -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
|
// 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.
|
// 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,
|
/// Manipulates app data,
|
||||||
abstract class DashboardApi {
|
abstract class DashboardApi {
|
||||||
ItemApi get items;
|
CategoryApi get categories;
|
||||||
EntryApi get entries;
|
EntryApi get entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manipulates [Item] data.
|
/// Manipulates [Category] data.
|
||||||
abstract class ItemApi {
|
abstract class CategoryApi {
|
||||||
Future<Item> delete(String id);
|
Future<Category> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Something being tracked.
|
Future<Category> get(String id);
|
||||||
class Item {
|
|
||||||
final String name;
|
|
||||||
String id;
|
|
||||||
|
|
||||||
Item(this.name);
|
Future<Category> insert(Category category);
|
||||||
|
|
||||||
|
Future<List<Category>> list();
|
||||||
|
|
||||||
|
Future<Category> update(Category category, String id);
|
||||||
|
|
||||||
|
Stream<List<Category>> subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manipulates [Entry] data.
|
/// Manipulates [Entry] data.
|
||||||
abstract class EntryApi {
|
abstract class EntryApi {
|
||||||
Future<Entry> delete(String itemId, String id);
|
Future<Entry> delete(String categoryId, String id);
|
||||||
Future<Entry> insert(String itemId, Entry entry);
|
|
||||||
Future<List<Entry>> list(String itemId);
|
Future<Entry> get(String categoryId, String id);
|
||||||
Future<Entry> update(String itemId, String id, Entry entry);
|
|
||||||
Stream<List<Entry>> allEntriesStream(String itemId);
|
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.
|
/// A number tracked at a point in time.
|
||||||
|
@JsonSerializable()
|
||||||
class Entry {
|
class Entry {
|
||||||
final int value;
|
int value;
|
||||||
final DateTime time;
|
@JsonKey(fromJson: _timestampToDateTime, toJson: _dateTimeToTimestamp)
|
||||||
|
DateTime time;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
Entry(this.value, this.time);
|
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>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
experimental/web_dashboard/lib/src/api/api.g.dart
Normal file
33
experimental/web_dashboard/lib/src/api/api.g.dart
Normal file
@@ -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
|
// 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
|
// 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.
|
// 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.
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:uuid/uuid.dart' as uuid;
|
import 'package:uuid/uuid.dart' as uuid;
|
||||||
|
|
||||||
@@ -13,48 +14,67 @@ class MockDashboardApi implements DashboardApi {
|
|||||||
final EntryApi entries = MockEntryApi();
|
final EntryApi entries = MockEntryApi();
|
||||||
|
|
||||||
@override
|
@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 {
|
class MockCategoryApi implements CategoryApi {
|
||||||
Map<String, Item> _storage = {};
|
Map<String, Category> _storage = {};
|
||||||
StreamController<List<Item>> _streamController =
|
StreamController<List<Category>> _streamController =
|
||||||
StreamController<List<Item>>.broadcast();
|
StreamController<List<Category>>.broadcast();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Item> delete(String id) async {
|
Future<Category> delete(String id) async {
|
||||||
|
var removed = _storage.remove(id);
|
||||||
_emit();
|
_emit();
|
||||||
return _storage.remove(id);
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Item> get(String id) async {
|
Future<Category> get(String id) async {
|
||||||
return _storage[id];
|
return _storage[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Item> insert(Item item) async {
|
Future<Category> insert(Category category) async {
|
||||||
var id = uuid.Uuid().v4();
|
var id = uuid.Uuid().v4();
|
||||||
var newItem = Item(item.name)..id = id;
|
var newCategory = Category(category.name)..id = id;
|
||||||
_storage[id] = newItem;
|
_storage[id] = newCategory;
|
||||||
_emit();
|
_emit();
|
||||||
return newItem;
|
return newCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Item>> list() async {
|
Future<List<Category>> list() async {
|
||||||
return _storage.values.toList();
|
return _storage.values.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Item> update(Item item, String id) async {
|
Future<Category> update(Category category, String id) async {
|
||||||
_storage[id] = item;
|
_storage[id] = category;
|
||||||
return item..id = id;
|
_emit();
|
||||||
|
return category..id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Item>> allItemsStream() {
|
Stream<List<Category>> subscribe() => _streamController.stream;
|
||||||
return _streamController.stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _emit() {
|
void _emit() {
|
||||||
_streamController.add(_storage.values.toList());
|
_streamController.add(_storage.values.toList());
|
||||||
@@ -63,44 +83,64 @@ class MockItemApi implements ItemApi {
|
|||||||
|
|
||||||
class MockEntryApi implements EntryApi {
|
class MockEntryApi implements EntryApi {
|
||||||
Map<String, Entry> _storage = {};
|
Map<String, Entry> _storage = {};
|
||||||
StreamController<List<Entry>> _streamController =
|
StreamController<_EntriesEvent> _streamController =
|
||||||
StreamController<List<Entry>>.broadcast();
|
StreamController.broadcast();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Entry> delete(String itemId, String id) async {
|
Future<Entry> delete(String categoryId, String id) async {
|
||||||
_emit();
|
_emit(categoryId);
|
||||||
return _storage.remove('$itemId-$id');
|
return _storage.remove('$categoryId-$id');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Entry> insert(String itemId, Entry entry) async {
|
Future<Entry> insert(String categoryId, Entry entry) async {
|
||||||
var id = uuid.Uuid().v4();
|
var id = uuid.Uuid().v4();
|
||||||
var newEntry = Entry(entry.value, entry.time)..id = id;
|
var newEntry = Entry(entry.value, entry.time)..id = id;
|
||||||
_storage['$itemId-$id'] = newEntry;
|
_storage['$categoryId-$id'] = newEntry;
|
||||||
_emit();
|
_emit(categoryId);
|
||||||
return newEntry;
|
return newEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Entry>> list(String itemId) async {
|
Future<List<Entry>> list(String categoryId) async {
|
||||||
return _storage.keys
|
return _storage.keys
|
||||||
.where((k) => k.startsWith(itemId))
|
.where((k) => k.startsWith(categoryId))
|
||||||
.map((k) => _storage[k])
|
.map((k) => _storage[k])
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Entry> update(String itemId, String id, Entry entry) async {
|
Future<Entry> update(String categoryId, String id, Entry entry) async {
|
||||||
_storage['$itemId-$id'] = entry;
|
_storage['$categoryId-$id'] = entry;
|
||||||
|
_emit(categoryId);
|
||||||
return entry..id = id;
|
return entry..id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<Entry>> allEntriesStream(String itemId) {
|
Stream<List<Entry>> subscribe(String categoryId) {
|
||||||
return _streamController.stream;
|
return _streamController.stream
|
||||||
|
.where((event) => event.categoryId == categoryId)
|
||||||
|
.map((event) => event.entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _emit() {
|
void _emit(String categoryId) {
|
||||||
_streamController.add(_storage.values.toList());
|
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
|
// 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.
|
// 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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'api/api.dart';
|
import 'api/api.dart';
|
||||||
|
import 'api/firebase.dart';
|
||||||
import 'api/mock.dart';
|
import 'api/mock.dart';
|
||||||
|
import 'auth/auth.dart';
|
||||||
|
import 'auth/firebase.dart';
|
||||||
|
import 'auth/mock.dart';
|
||||||
import 'pages/home.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 {
|
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
|
@override
|
||||||
_DashboardAppState createState() => _DashboardAppState();
|
_DashboardAppState createState() => _DashboardAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardAppState extends State<DashboardApp> {
|
class _DashboardAppState extends State<DashboardApp> {
|
||||||
int _pageIndex = 0;
|
AppState _appState;
|
||||||
|
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_appState = AppState(widget.auth);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiProvider(
|
return Provider.value(
|
||||||
providers: [
|
value: _appState,
|
||||||
Provider<DashboardApi>(create: (_) => MockDashboardApi()),
|
|
||||||
],
|
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: AdaptiveScaffold(
|
home: Builder(
|
||||||
currentIndex: _pageIndex,
|
builder: (context) => SignInPage(
|
||||||
destinations: [
|
auth: _appState.auth,
|
||||||
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
|
onSuccess: (user) => _handleSignIn(user, context, _appState),
|
||||||
AdaptiveScaffoldDestination(title: 'Entries', icon: Icons.list),
|
),
|
||||||
AdaptiveScaffoldDestination(
|
|
||||||
title: 'Settings', icon: Icons.settings),
|
|
||||||
],
|
|
||||||
body: _pageAtIndex(_pageIndex),
|
|
||||||
onNavigationIndexChange: (newIndex) {
|
|
||||||
setState(() {
|
|
||||||
_pageIndex = newIndex;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget _pageAtIndex(int index) {
|
/// Sets the DashboardApi on AppState and navigates to the home page.
|
||||||
switch (index) {
|
void _handleSignIn(User user, BuildContext context, AppState appState) {
|
||||||
case 1:
|
appState.api = widget.apiBuilder(user);
|
||||||
return Center(child: Text('page 2'));
|
|
||||||
case 2:
|
_showPage(HomePage(), context);
|
||||||
return Center(child: Text('page 3'));
|
}
|
||||||
case 0:
|
|
||||||
default:
|
/// Navigates to the home page using a fade transition.
|
||||||
return HomePage();
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
experimental/web_dashboard/lib/src/auth/auth.dart
Normal file
12
experimental/web_dashboard/lib/src/auth/auth.dart
Normal file
@@ -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;
|
||||||
|
}
|
||||||
41
experimental/web_dashboard/lib/src/auth/firebase.dart
Normal file
41
experimental/web_dashboard/lib/src/auth/firebase.dart
Normal file
@@ -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);
|
||||||
|
}
|
||||||
21
experimental/web_dashboard/lib/src/auth/mock.dart
Normal file
21
experimental/web_dashboard/lib/src/auth/mock.dart
Normal file
@@ -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";
|
||||||
|
}
|
||||||
67
experimental/web_dashboard/lib/src/pages/dashboard.dart
Normal file
67
experimental/web_dashboard/lib/src/pages/dashboard.dart
Normal file
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
161
experimental/web_dashboard/lib/src/pages/entries.dart
Normal file
161
experimental/web_dashboard/lib/src/pages/entries.dart
Normal file
@@ -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.
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import '../api/api.dart';
|
import '../widgets/dialogs.dart';
|
||||||
import 'item_details.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) {
|
Widget build(BuildContext context) {
|
||||||
var api = Provider.of<DashboardApi>(context);
|
return AdaptiveScaffold(
|
||||||
|
currentIndex: _pageIndex,
|
||||||
return Scaffold(
|
destinations: [
|
||||||
body: StreamProvider<List<Item>>(
|
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
|
||||||
initialData: [],
|
AdaptiveScaffoldDestination(title: 'Entries', icon: Icons.list),
|
||||||
create: (context) => api.items.allItemsStream(),
|
AdaptiveScaffoldDestination(title: 'Settings', icon: Icons.settings),
|
||||||
child: Consumer<List<Item>>(
|
],
|
||||||
builder: (context, items, child) {
|
body: _pageAtIndex(_pageIndex),
|
||||||
return ListView.builder(
|
onNavigationIndexChange: (newIndex) {
|
||||||
itemBuilder: (context, idx) {
|
setState(() {
|
||||||
return ListTile(
|
_pageIndex = newIndex;
|
||||||
title: Text(items[idx].name),
|
});
|
||||||
onTap: () {
|
},
|
||||||
_showDetails(items[idx], context);
|
floatingActionButton:
|
||||||
},
|
_hasFloatingActionButton ? _buildFab(context) : null,
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: items.length,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
child: Icon(Icons.add),
|
|
||||||
onPressed: () {
|
|
||||||
api.items.insert(Item('Coffees Drank'));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDetails(Item item, BuildContext context) {
|
bool get _hasFloatingActionButton {
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
if (_pageIndex == 2) return false;
|
||||||
return ItemDetailsPage(item);
|
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}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
experimental/web_dashboard/lib/src/pages/sign_in.dart
Normal file
46
experimental/web_dashboard/lib/src/pages/sign_in.dart
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
experimental/web_dashboard/lib/src/utils/chart_utils.dart
Normal file
65
experimental/web_dashboard/lib/src/utils/chart_utils.dart
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
15
experimental/web_dashboard/lib/src/utils/day_helpers.dart
Normal file
15
experimental/web_dashboard/lib/src/utils/day_helpers.dart
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
experimental/web_dashboard/lib/src/widgets/category_chart.dart
Normal file
111
experimental/web_dashboard/lib/src/widgets/category_chart.dart
Normal file
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
experimental/web_dashboard/lib/src/widgets/category_forms.dart
Normal file
103
experimental/web_dashboard/lib/src/widgets/category_forms.dart
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
experimental/web_dashboard/lib/src/widgets/dialogs.dart
Normal file
98
experimental/web_dashboard/lib/src/widgets/dialogs.dart
Normal file
@@ -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();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
experimental/web_dashboard/lib/src/widgets/edit_entry.dart
Normal file
154
experimental/web_dashboard/lib/src/widgets/edit_entry.dart
Normal file
@@ -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
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "2.1.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.39.8"
|
version: "0.39.6"
|
||||||
archive:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: archive
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.13"
|
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -43,6 +36,62 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -50,6 +99,69 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.3"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -92,6 +204,76 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -102,6 +284,11 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -109,6 +296,41 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
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:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -122,7 +344,7 @@ packages:
|
|||||||
name: http
|
name: http
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.1"
|
version: "0.12.0+4"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -137,13 +359,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
image:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: intl
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.12"
|
version: "0.16.1"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -158,6 +380,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1+1"
|
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:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -213,7 +449,7 @@ packages:
|
|||||||
name: node_io
|
name: node_io
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.0.1+2"
|
||||||
node_preamble:
|
node_preamble:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -234,7 +470,7 @@ packages:
|
|||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.4"
|
version: "1.7.0"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -242,13 +478,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
petitparser:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: plugin_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "1.0.2"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -262,7 +498,7 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.0.4"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -270,6 +506,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.4"
|
version: "1.4.4"
|
||||||
|
pubspec_parse:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pubspec_parse
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.5"
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -310,6 +553,13 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
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:
|
source_map_stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -345,6 +595,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -365,7 +622,7 @@ packages:
|
|||||||
name: test
|
name: test
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.14.3"
|
version: "1.14.2"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -379,7 +636,14 @@ packages:
|
|||||||
name: test_core
|
name: test_core
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -407,14 +671,14 @@ packages:
|
|||||||
name: vm_service
|
name: vm_service
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.7+15"
|
version: "0.9.7+14"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -428,21 +692,14 @@ packages:
|
|||||||
name: webkit_inspection_protocol
|
name: webkit_inspection_protocol
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.3"
|
version: "0.5.0+1"
|
||||||
xml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.6.1"
|
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.7.0 <3.0.0"
|
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
|
name: web_dashboard
|
||||||
description: A desktop-friendly dashboard app
|
description: A dashboard app sample
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.3.0 <3.0.0"
|
sdk: ">=2.6.0 <3.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
cloud_firestore: ^0.13.0
|
||||||
cupertino_icons: ^0.1.2
|
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
|
provider: ^4.0.0
|
||||||
uuid: ^2.0.0
|
uuid: ^2.0.0
|
||||||
|
charts_flutter: ^0.9.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
test: any
|
build_runner: ^1.8.0
|
||||||
|
json_serializable: ^3.3.0
|
||||||
|
test: ^1.14.0
|
||||||
|
grinder: ^0.8.4
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
29
experimental/web_dashboard/test/chart_utils_test.dart
Normal file
29
experimental/web_dashboard/test/chart_utils_test.dart
Normal file
@@ -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', () {
|
group('items', () {
|
||||||
test('insert', () async {
|
test('insert', () async {
|
||||||
var item = await api.items.insert(Item('Coffees Drank'));
|
var category = await api.categories.insert(Category('Coffees Drank'));
|
||||||
expect(item.name, 'Coffees Drank');
|
expect(category.name, 'Coffees Drank');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete', () async {
|
test('delete', () async {
|
||||||
await api.items.insert(Item('Coffees Drank'));
|
await api.categories.insert(Category('Coffees Drank'));
|
||||||
var item2 = await api.items.insert(Item('Miles Ran'));
|
var category = await api.categories.insert(Category('Miles Ran'));
|
||||||
var removed = await api.items.delete(item2.id);
|
var removed = await api.categories.delete(category.id);
|
||||||
|
|
||||||
expect(removed.name, 'Miles Ran');
|
expect(removed.name, 'Miles Ran');
|
||||||
|
|
||||||
var items = await api.items.list();
|
var categories = await api.categories.list();
|
||||||
expect(items, hasLength(1));
|
expect(categories, hasLength(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('update', () async {
|
test('update', () async {
|
||||||
var item = await api.items.insert(Item('Coffees Drank'));
|
var category = await api.categories.insert(Category('Coffees Drank'));
|
||||||
await api.items.update(Item('Bagels Consumed'), item.id);
|
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'));
|
expect(latest.name, equals('Bagels Consumed'));
|
||||||
});
|
});
|
||||||
test('subscribe', () async {
|
test('subscribe', () async {
|
||||||
var stream = api.items.allItemsStream();
|
var stream = api.categories.subscribe();
|
||||||
|
|
||||||
stream.listen(expectAsync1((x) {
|
stream.listen(expectAsync1((x) {
|
||||||
expect(x, hasLength(1));
|
expect(x, hasLength(1));
|
||||||
expect(x.first.name, equals('Coffees Drank'));
|
expect(x.first.name, equals('Coffees Drank'));
|
||||||
}, count: 1));
|
}, count: 1));
|
||||||
await api.items.insert(Item('Coffees Drank'));
|
await api.categories.insert(Category('Coffees Drank'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('entry service', () {
|
group('entry service', () {
|
||||||
Item item;
|
Category category;
|
||||||
DateTime dateTime = DateTime(2020, 1, 1, 30, 45);
|
DateTime dateTime = DateTime(2020, 1, 1, 30, 45);
|
||||||
|
|
||||||
setUp(() async {
|
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 {
|
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.value, 1);
|
||||||
expect(entry.time, dateTime);
|
expect(entry.time, dateTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete', () async {
|
test('delete', () async {
|
||||||
await api.entries.insert(item.id, Entry(1, dateTime));
|
await api.entries.insert(category.id, Entry(1, dateTime));
|
||||||
var entry2 = await api.entries.insert(item.id, Entry(2, 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));
|
expect(entries, hasLength(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('update', () async {
|
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 =
|
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);
|
expect(updated.value, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('subscribe', () async {
|
test('subscribe', () async {
|
||||||
var stream = api.entries.allEntriesStream(item.id);
|
var stream = api.entries.subscribe(category.id);
|
||||||
|
|
||||||
stream.listen(expectAsync1((x) {
|
stream.listen(expectAsync1((x) {
|
||||||
expect(x, hasLength(1));
|
expect(x, hasLength(1));
|
||||||
expect(x.first.value, equals(1));
|
expect(x.first.value, equals(1));
|
||||||
}, count: 1));
|
}, count: 1));
|
||||||
|
|
||||||
api.entries.insert(item.id, Entry(1, dateTime));
|
await api.entries.insert(category.id, Entry(1, dateTime));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
108
experimental/web_dashboard/tool/grind.dart
Normal file
108
experimental/web_dashboard/tool/grind.dart
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
experimental/web_dashboard/web/firebase_init.js
Normal file
12
experimental/web_dashboard/web/firebase_init.js
Normal 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>
|
<title>web_dashboard</title>
|
||||||
<link rel="manifest" href="/manifest.json">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||||
|
|||||||
11
material_theme_builder/test/widget_test.dart
Normal file
11
material_theme_builder/test/widget_test.dart
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user