32 Commits

Author SHA1 Message Date
c43316be6d Implemented New Screens in Dashboard Screen
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-10 21:10:05 +01:00
ddf9e981fd Created SettingsScreen
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-10 21:09:39 +01:00
32432bb138 Started using Platform Text
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-10 21:09:16 +01:00
df46901f6d Created ActiveTimeProgressesScreen
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-10 21:08:45 +01:00
b813481369 Created ActiveTimeProgressesScreen and migrated ListView and Item to Platform Specific
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-09 20:12:26 +01:00
ed5d2b92f7 Started implementing flutter_platform_widgets
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-09 19:32:39 +01:00
40bdcc44f9 Feature/code cleanup (#9)
* Implemented hasSettingsLoaded reducer
* Added Padding to Progress List View
* Created Settings and Time Progress List Store Connector
* Rewritten Home Active Tab
* Fixed missing onTap in Progress List Tile
* Started using new Store Connectors in Inactive and Settings Tab
* Created Time Progress Store Connector
* Rewritten ProgressDetailScreen with new Store Connectors
* Rewritten DatePickerBtn with TextButton
* Deleted unused widget
* Changed Foreground Color behaviour in ColorPicker BTN
* Created Select Duration Button
* Rewritten Duration Setting Widget
* Updated Version Number

Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-08 20:48:45 +01:00
fc35476503 Feature/widget testing (#8)
* Configured iOS Build
* Created ProgressListTile widget
* Created MaterialTesterWidget
* Created String Methods for Testing in ProgressListTile
* Created ProgressListTileStrings class
* Using Progress List Tile
* Created Progress List View
* Created Progress List Tile currently, future and past test.
* Created Progress List View one and five Time Progresses test.

Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-03-08 16:19:31 +01:00
90f2998088 Feature/default duration setting (#7)
* Implemented Basic Duration Settings into AppSettings Model

* Implemented Basic Duration Settings into AppSettings Model

* Created Duration Settings Widget and Started using ViewModel in HomeSettingsTab

* Updated Version Number
2021-03-03 19:59:33 +01:00
b520d56d1a Feature/change progress colors (#6)
* Added Settings Actions
* Created App Settings and Repo + Entity
* Code cleanup Time Progress
* Created App Settings Middleware
* Has Progresses ad has Settings loaded
* Created Load and Update Settings reducers
* Added Settings store middleware to renamed store middleware
* Load Default Settings if not Saved. Use Redux AppState to showprogress colors.
Colors are not yet changeable.
* Added ColorPicker for Done and Left Color
Fixed Loading App Settings Bug
* Fixed Version Number
* Fixed Android App Logo
* Extracted Color Settings into Widget
* Fixed Home Settings Tab Layout and Color Settings Button now show Text in complementary color
2021-03-03 16:35:08 +01:00
c580e45361 Fixed App Logo
Signed-of-by : Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-24 02:06:34 +01:00
238fd6bc6a Merge remote-tracking branch 'origin/main' into main 2021-02-23 23:15:49 +01:00
478ea644a9 Added new App Icon
Signed-of-by : Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-23 23:15:31 +01:00
82fc8c5fbb Added new App Icon
Signed-of-by : Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-23 23:14:51 +01:00
c23863903a Updated Icon for iOS
Singed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-22 00:44:11 +01:00
65939fba30 Updated gitignore for ios
Singed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-22 00:42:55 +01:00
f592e7b66b Fixed Progress Editor Bug
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-13 00:54:16 +01:00
9ee29a7c7c Code cleanup
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-12 00:34:14 +01:00
d06d7f1448 Code cleanup
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-12 00:17:19 +01:00
5c2592f601 Cleanup of Progress Detail Screen
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 23:38:02 +01:00
c09b164a61 Capped Percent Done Value between 0 and 1.
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 20:28:40 +01:00
1826bbe421 Created ProgressViewWidget and rewritten ProgressDetailScreen with it
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 20:25:24 +01:00
1abfd0c1f5 Created ProgressEditorWidget and rewritten ProgressCreationScreen with it
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 18:35:47 +01:00
763cf9d627 Created DatePickerBtn and Started Using it in ProgressCreationScreen
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 17:05:23 +01:00
6dde08d74d Fixed Delete Time Progress Bug
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 15:30:48 +01:00
fad8d8b49b Fixed Navigator Bug in new App Layout
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 15:06:25 +01:00
d289cfc4d5 Renamed ProgressDashboardScreen to HomeScreen
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-11 15:01:53 +01:00
409ccbcdda New App Layout
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-10 19:36:04 +01:00
c5e240e813 Started Using ThemeData, Changed Primary Color to indigo and corrected logo to indigo.
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-10 15:04:51 +01:00
44db690a4a Replaced Logo, forgot to run flutter_launcher_icons:main
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-09 17:33:39 +01:00
8156b3f529 Replaced Logo
Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-02-09 17:19:49 +01:00
58bc713227 Feature/bugfix 02 past time progresses (#5)
* Replaced startedTimeProgressSelectors with currentTimeProgressSelector.
* Added pastTimeProgressSelector.
* Fixed ProgressDetailScreen for PastProgresses.
* Fixed ProgressDashBoard for PastProgresses.
* Fixed AppDrawer for PastProgresses.
* Increased Version Number

Signed-off-by: Andreas Fahrecker <AndreasFahrecker@gmail.com>
2021-01-28 23:14:45 +01:00
105 changed files with 2348 additions and 1112 deletions

97
.gitignore vendored
View File

@ -1,5 +1,6 @@
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
@ -15,30 +16,102 @@
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Visual Studio Code related
.classpath
.project
.settings/
.vscode/
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-cache/
.pub/
/build/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Web related
lib/generated_plugin_registrant.dart
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# Symbolication related
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/macos/Flutter/GeneratedPluginRegistrant.swift
# Coverage
coverage/
# Symbols
app.*.symbols
# Obfuscation related
app.*.map.json
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock

View File

@ -0,0 +1,6 @@
package com.fahrecker.time_progress_tracker
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/logo/android_1024.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
assets/logo/ios_1024.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -1,4 +1,5 @@
flutter_icons:
android: true
ios: false
image_path: "assets/icons/launcher_icon.png"
ios: true
image_path_android: "assets/logo/android_1024.png"
image_path_ios: "assets/logo/ios_1024.png"

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>

View File

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

View File

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

41
ios/Podfile Normal file
View File

@ -0,0 +1,41 @@
# 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 flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@ -3,14 +3,14 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
6186AFFAE7FCA76C81CF360E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D5EAE1ADE1FFBE7D23EE84E /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -32,18 +32,21 @@
/* Begin PBXFileReference section */
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>"; };
1DBA7F16BF734A3CE98E5546 /* 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>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; 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>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
7D5EAE1ADE1FFBE7D23EE84E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; 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>"; };
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>"; };
B4DFA246891E5346CCC8628F /* 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>"; };
DC45E65269FD602449E1FEBD /* 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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -51,12 +54,23 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6186AFFAE7FCA76C81CF360E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
7425B1E33E3BCBE464E2CBB5 /* Pods */ = {
isa = PBXGroup;
children = (
B4DFA246891E5346CCC8628F /* Pods-Runner.debug.xcconfig */,
1DBA7F16BF734A3CE98E5546 /* Pods-Runner.release.xcconfig */,
DC45E65269FD602449E1FEBD /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@ -74,7 +88,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
7425B1E33E3BCBE464E2CBB5 /* Pods */,
E3A24E042363B3BCA7910470 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -89,25 +104,24 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
E3A24E042363B3BCA7910470 /* Frameworks */ = {
isa = PBXGroup;
children = (
97C146F21CF9000F007C117D /* main.m */,
7D5EAE1ADE1FFBE7D23EE84E /* Pods_Runner.framework */,
);
name = "Supporting Files";
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
@ -117,12 +131,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
A60954191C254DCAC69F1735 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
555CAADD55712EED12802136 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -144,6 +160,7 @@
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
@ -194,6 +211,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
555CAADD55712EED12802136 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -208,6 +242,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
A60954191C254DCAC69F1735 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -215,8 +271,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
97C146F31CF9000F007C117D /* main.m in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -284,7 +339,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -298,20 +353,19 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RD9K843SK5;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.fahrecker.timeProgressCalculator;
PRODUCT_BUNDLE_IDENTIFIER = com.fahrecker.timeProgressTracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
@ -363,7 +417,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -412,10 +466,12 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@ -426,20 +482,20 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RD9K843SK5;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.fahrecker.timeProgressCalculator;
PRODUCT_BUNDLE_IDENTIFIER = com.fahrecker.timeProgressTracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@ -449,20 +505,19 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RD9K843SK5;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.fahrecker.timeProgressCalculator;
PRODUCT_BUNDLE_IDENTIFIER = com.fahrecker.timeProgressTracker;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;

View File

@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
location = "self:">
</FileRef>
</Workspace>

View File

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

View File

@ -1,6 +0,0 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end

View File

@ -1,13 +0,0 @@
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end

View File

@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -4,6 +4,8 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Time Progress Tracker</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -11,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>time_progress_calculator</string>
<string>time_progress_tracker</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -1,9 +0,0 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@ -1,7 +1,24 @@
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
class LoadSettingsAction {}
class AppSettingsLoadedActions {
final AppSettings appSettings;
AppSettingsLoadedActions(this.appSettings);
}
class UpdateAppSettingsActions {
final AppSettings appSettings;
UpdateAppSettingsActions(this.appSettings);
}
class AppSettingsNotLoadedAction {}
class LoadTimeProgressListAction {}
class TimeProgressListLoadedAction {
@ -32,7 +49,10 @@ class DeleteTimeProgressAction {
}
void loadTimeProgressListIfUnloaded(Store<AppState> store) {
if (!store.state.hasLoaded) {
if (!store.state.hasProgressesLoaded)
store.dispatch(LoadTimeProgressListAction());
}
}
void loadSettingsIfUnloaded(Store<AppState> store) {
if (!store.state.hasSettingsLoaded) store.dispatch(LoadSettingsAction());
}

View File

@ -1,48 +1,31 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/screens/progress_creation_screen.dart';
import 'package:time_progress_tracker/screens/progress_dashboard_screen.dart';
import 'package:time_progress_tracker/screens/progress_detail_screen.dart';
import 'package:time_progress_tracker/screens/dashboard_screen.dart';
import 'package:time_progress_tracker/utils/color_utils.dart';
class TimeProgressTrackerApp extends StatelessWidget {
static const String name = "Time Progress Tracker";
final Store<AppState> store;
final String appVersion;
TimeProgressTrackerApp({
Key key,
this.store,
this.appVersion,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreProvider(
store: store,
child: MaterialApp(
child: PlatformApp(
title: name,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: ProgressDashboardScreen.routeName,
routes: {
ProgressDashboardScreen.routeName: (BuildContext context) =>
ProgressDashboardScreen(
appVersion: appVersion,
),
ProgressDetailScreen.routeName: (BuildContext context) =>
ProgressDetailScreen(
appVersion: appVersion,
),
ProgressCreationScreen.routeName: (BuildContext context) =>
ProgressCreationScreen(
appVersion: appVersion,
),
},
home: DashboardScreen(),
material: (_, __) => MaterialAppData(theme: materialThemeData),
cupertino: (_, __) => CupertinoAppData(theme: cupertinoThemeData),
),
);
}

17
lib/helper_functions.dart Normal file
View File

@ -0,0 +1,17 @@
import 'dart:ui';
import 'package:time_progress_tracker/models/time_progress.dart';
TimeProgress selectProgressById(List<TimeProgress> tpList, String id) =>
tpList.firstWhere((tp) => tp.id == id, orElse: null);
List<TimeProgress> selectActiveProgresses(List<TimeProgress> tpList) =>
tpList.where((tp) => tp.hasStarted() && !tp.hasEnded()).toList();
List<TimeProgress> selectInactiveProgresses(List<TimeProgress> tpList) =>
tpList.where((tp) => !tp.hasStarted() || tp.hasEnded()).toList();
bool useBrightBackground(Color bC) {
double yiq = ((bC.red * 299) + (bC.green * 587) + (bC.blue * 114)) / 1000;
return yiq >= 186 || (bC.red == 0 && bC.green == 0 && bC.blue == 0);
}

View File

@ -1,24 +1,24 @@
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
import 'package:redux/redux.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:time_progress_tracker/app.dart';
import 'package:time_progress_tracker/middleware/store_time_progress_middleware.dart';
import 'package:time_progress_tracker/middleware/store_middleware.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/persistence/app_settings.dart';
import 'package:time_progress_tracker/persistence/time_progress_repository.dart';
import 'package:time_progress_tracker/reducers/app_state_reducer.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
runApp(TimeProgressTrackerApp(
store: Store<AppState>(
appStateReducer,
initialState: AppState.initial(),
middleware: createStoreTimeProgressListMiddleware(
TimeProgressRepository(await SharedPreferences.getInstance()),
),
middleware: createStoreMiddleware(
TimeProgressRepository(prefs), AppSettingsRepository(prefs)),
),
appVersion: (await PackageInfo.fromPlatform()).version,
));
}

View File

@ -1,21 +1,28 @@
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/persistence/app_settings.dart';
import 'package:time_progress_tracker/persistence/time_progress_entity.dart';
import 'package:time_progress_tracker/persistence/time_progress_repository.dart';
import 'package:time_progress_tracker/selectors/time_progress_selectors.dart';
List<Middleware<AppState>> createStoreTimeProgressListMiddleware(
TimeProgressRepository repository) {
final saveTimeProgressList = _createSaveTimeProgressList(repository);
final loadTimeProgressList = _createLoadTimeProgressList(repository);
List<Middleware<AppState>> createStoreMiddleware(
TimeProgressRepository progressRepo, AppSettingsRepository settingsRepo) {
final saveTimeProgressList = _createSaveTimeProgressList(progressRepo);
final loadTimeProgressList = _createLoadTimeProgressList(progressRepo);
final saveSettings = _createSaveAppSettings(settingsRepo);
final loadSettings = _createLoadAppSettings(settingsRepo);
return [
TypedMiddleware<AppState, LoadTimeProgressListAction>(loadTimeProgressList),
TypedMiddleware<AppState, AddTimeProgressAction>(saveTimeProgressList),
TypedMiddleware<AppState, UpdateTimeProgressAction>(saveTimeProgressList),
TypedMiddleware<AppState, DeleteTimeProgressAction>(saveTimeProgressList),
TypedMiddleware<AppState, LoadSettingsAction>(loadSettings),
TypedMiddleware<AppState, UpdateAppSettingsActions>(saveSettings)
];
}
@ -47,3 +54,17 @@ Middleware<AppState> _createLoadTimeProgressList(
}).catchError((_) => store.dispatch(TimeProgressListNotLoadedAction()));
};
}
Middleware<AppState> _createSaveAppSettings(AppSettingsRepository repo) =>
(Store<AppState> store, dynamic action, NextDispatcher next) {
next(action);
repo.saveAppSettings(store.state.appSettings.toEntity());
};
Middleware<AppState> _createLoadAppSettings(AppSettingsRepository repo) =>
(Store<AppState> store, dynamic action, NextDispatcher next) {
repo.loadAppSettings().then((appSettings) {
store.dispatch(
AppSettingsLoadedActions(AppSettings.fromEntity(appSettings)));
});
};

View File

@ -5,3 +5,27 @@ class TimeProgressInvalidNameException implements Exception {
String errMsg() => "The name of a TimeProgress can't be: $invalidName";
}
class TimeProgressStartTimeIsNotBeforeEndTimeException implements Exception {
final startTime;
final endTime;
TimeProgressStartTimeIsNotBeforeEndTimeException(
this.startTime, this.endTime);
String errMsg() =>
"The Start Time has to be before the end time. Therefore these values are"
" invalid: Start Time: $startTime EndTime: $endTime";
}
class TimeProgressHasStartedException implements Exception {
String errMsg() =>
"This TimeProgress has started. Therefore all calculation, which assume, "
"that the progress hasn't started yet can't be performed";
}
class TimeProgressHasNotEndedException implements Exception {
String errMsg() =>
"This TimeProgress hasn't ended. Therefore all calculation, which assume,"
" that the progress has ended already can't be performed";
}

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/persistence/app_settings.dart';
@immutable
class AppSettings {
final Color doneColor;
final Color leftColor;
final Duration duration;
AppSettings({
this.doneColor,
this.leftColor,
this.duration,
});
factory AppSettings.defaults() => AppSettings(
doneColor: Colors.green,
leftColor: Colors.red,
duration: Duration(days: 365),
);
AppSettings copyWith({
Color doneColor,
Color leftColor,
Duration duration,
}) =>
AppSettings(
doneColor: doneColor ?? this.doneColor,
leftColor: leftColor ?? this.leftColor,
duration: duration ?? this.duration,
);
@override
int get hashCode =>
doneColor.hashCode ^ leftColor.hashCode ^ duration.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppSettings &&
runtimeType == other.runtimeType &&
doneColor == other.doneColor &&
leftColor == other.leftColor &&
duration == other.duration;
AppSettingsEntity toEntity() =>
AppSettingsEntity(doneColor.value, leftColor.value, duration.inDays);
static AppSettings fromEntity(AppSettingsEntity entity) => AppSettings(
doneColor: Color(entity.doneColorValue),
leftColor: Color(entity.leftColorValue),
duration: Duration(days: entity.durationDays),
);
}

View File

@ -1,36 +1,40 @@
import 'package:meta/meta.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
@immutable
class AppState {
final bool hasLoaded;
final bool hasProgressesLoaded, hasSettingsLoaded;
final List<TimeProgress> timeProgressList;
final AppSettings appSettings;
AppState({
this.hasLoaded = false,
this.timeProgressList = const [],
});
AppState(
{this.hasProgressesLoaded = false,
this.hasSettingsLoaded = false,
this.timeProgressList = const [],
this.appSettings});
factory AppState.initial() => AppState(hasLoaded: false);
factory AppState.initial() =>
AppState(hasProgressesLoaded: false, appSettings: AppSettings.defaults());
AppState copyWith({
bool hasLoaded,
List<TimeProgress> timeProgressList,
}) {
return AppState(
hasLoaded: hasLoaded ?? this.hasLoaded,
hasProgressesLoaded: hasLoaded ?? this.hasProgressesLoaded,
timeProgressList: timeProgressList ?? this.timeProgressList,
);
}
@override
int get hashCode => hasLoaded.hashCode ^ timeProgressList.hashCode;
int get hashCode => hasProgressesLoaded.hashCode ^ timeProgressList.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppState &&
runtimeType == other.runtimeType &&
hasLoaded == other.hasLoaded &&
hasProgressesLoaded == other.hasProgressesLoaded &&
timeProgressList == other.timeProgressList;
}

View File

@ -11,41 +11,53 @@ class TimeProgress {
final DateTime endTime;
TimeProgress(this.name, this.startTime, this.endTime, {String id})
: id = id ?? Uuid().generateV4() {
if (this.name == null || this.name == "") {
throw new TimeProgressInvalidNameException(this.name);
}
}
: id = id ?? Uuid().generateV4();
factory TimeProgress.initialDefault() {
int thisYear = DateTime.now().year;
return TimeProgress("Initial Name", DateTime(thisYear - 1), DateTime(thisYear + 1));
return TimeProgress(
"Initial Name", DateTime(thisYear - 1), DateTime(thisYear + 1));
}
factory TimeProgress.defaultFromDuration(Duration duration) =>
TimeProgress("", DateTime.now(), DateTime.now().add(duration));
TimeProgress copyWith(
{String id, String name, DateTime startTime, DateTime endTime}) {
return TimeProgress(
name ?? this.name,
startTime ?? this.startTime,
endTime ?? this.endTime,
id: id ?? this.id,
);
}
{String id, String name, DateTime startTime, DateTime endTime}) =>
TimeProgress(
name ?? this.name,
startTime ?? this.startTime,
endTime ?? this.endTime,
id: id ?? this.id,
);
int daysBehind() {
return DateTime.now().difference(startTime).inDays;
}
int daysBehind() => DateTime.now().difference(startTime).inDays;
int daysLeft() {
return endTime.difference(DateTime.now()).inDays;
}
int daysLeft() => endTime.difference(DateTime.now()).inDays;
int allDays() {
return endTime.difference(startTime).inDays;
}
int allDays() => endTime.difference(startTime).inDays;
double percentDone() {
return this.daysBehind() / (this.allDays() / 100) / 100;
double percent = this.daysBehind() / (this.allDays() / 100) / 100;
if (percent < 0) percent = 0;
if (percent > 1) percent = 1;
return percent;
}
bool hasStarted() =>
DateTime.now().millisecondsSinceEpoch > startTime.millisecondsSinceEpoch;
int daysTillStart() {
if (hasStarted()) throw new TimeProgressHasStartedException();
return startTime.difference(DateTime.now()).inDays;
}
bool hasEnded() =>
DateTime.now().millisecondsSinceEpoch > endTime.millisecondsSinceEpoch;
int daysSinceEnd() {
if (!hasEnded()) throw new TimeProgressHasNotEndedException();
return DateTime.now().difference(endTime).inDays;
}
@override
@ -63,20 +75,29 @@ class TimeProgress {
endTime == other.endTime;
@override
String toString() {
return "TimeProgress{id: $id, name: $name, startTime: $startTime, endTime: $endTime}";
}
String toString() =>
"TimeProgress{id: $id, name: $name, startTime: $startTime, endTime: $endTime}";
TimeProgressEntity toEntity() {
if (!TimeProgress.isNameValid(name))
throw new TimeProgressInvalidNameException(name);
if (!TimeProgress.areTimesValid(startTime, endTime))
throw new TimeProgressStartTimeIsNotBeforeEndTimeException(
startTime, endTime);
return TimeProgressEntity(id, name, startTime, endTime);
}
static TimeProgress fromEntity(TimeProgressEntity entity) {
return TimeProgress(
entity.name,
entity.startTime,
entity.endTime,
id: entity.id ?? Uuid().generateV4(),
);
}
static TimeProgress fromEntity(TimeProgressEntity entity) =>
TimeProgress(entity.name, entity.startTime, entity.endTime,
id: entity.id ?? Uuid().generateV4());
static bool isValid(TimeProgress tp) =>
TimeProgress.isNameValid(tp.name) &&
TimeProgress.areTimesValid(tp.startTime, tp.endTime);
static bool isNameValid(String name) =>
name != null && name != "" && name.length > 2 && name.length < 21;
static bool areTimesValid(DateTime startTime, DateTime endTime) =>
startTime.isBefore(endTime);
}

View File

@ -0,0 +1,59 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
class AppSettingsRepository {
static const String _key = "app_settings";
final SharedPreferences prefs;
final JsonCodec codec;
AppSettingsRepository(this.prefs, {this.codec = json});
Future<AppSettingsEntity> loadAppSettings() {
final String jsonString = this.prefs.getString(_key);
if (jsonString == null)
return Future<AppSettingsEntity>.value(AppSettingsEntity.defaults());
return Future<AppSettingsEntity>.value(
AppSettingsEntity.fromJson(codec.decode(jsonString)));
}
Future<bool> saveAppSettings(AppSettingsEntity appSettings) =>
this.prefs.setString(_key, codec.encode(appSettings));
}
class AppSettingsEntity {
static const String _doneKey = "doneColorValue",
_leftKey = "leftColorValue",
_durationDaysKey = "durationDays";
final int doneColorValue, leftColorValue, durationDays;
AppSettingsEntity(
this.doneColorValue, this.leftColorValue, this.durationDays);
factory AppSettingsEntity.defaults() => AppSettings.defaults().toEntity();
@override
int get hashCode => doneColorValue.hashCode ^ leftColorValue.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppSettingsEntity &&
runtimeType == other.runtimeType &&
doneColorValue == other.doneColorValue &&
leftColorValue == other.leftColorValue;
Map<String, Object> toJson() => {
_doneKey: doneColorValue,
_leftKey: leftColorValue,
_durationDaysKey: durationDays,
};
static AppSettingsEntity fromJson(Map<String, Object> json) =>
AppSettingsEntity(
json[_doneKey],
json[_leftKey],
json[_durationDaysKey],
);
}

View File

@ -2,8 +2,6 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:time_progress_tracker/persistence/time_progress_entity.dart';
import 'dart:developer' as developer;
class TimeProgressRepository {
static const String _key = "time_progress_repo";
final SharedPreferences prefs;

View File

@ -1,10 +1,35 @@
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/reducers/has_loaded_reducer.dart';
import 'package:time_progress_tracker/reducers/time_progress_list_reducer.dart';
AppState appStateReducer(AppState state, dynamic action) {
return AppState(
hasLoaded: hasLoadedReducer(state.hasLoaded, action),
hasSettingsLoaded:
hasSettingsLoadedReducer(state.hasSettingsLoaded, action),
hasProgressesLoaded:
hasProgressesLoadedReducer(state.hasProgressesLoaded, action),
timeProgressList: timeProgressListReducer(state.timeProgressList, action),
appSettings: appSettingsReducers(state.appSettings, action),
);
}
final appSettingsReducers = combineReducers<AppSettings>([
TypedReducer<AppSettings, AppSettingsLoadedActions>(_loadAppSettings),
TypedReducer<AppSettings, UpdateAppSettingsActions>(_updateAppSettings),
TypedReducer<AppSettings, AppSettingsNotLoadedAction>(_setDefaultSettings)
]);
AppSettings _loadAppSettings(
AppSettings appSettings, AppSettingsLoadedActions nS) =>
nS.appSettings;
AppSettings _setDefaultSettings(
AppSettings appSettings, AppSettingsNotLoadedAction action) =>
AppSettings.defaults();
AppSettings _updateAppSettings(
AppSettings appSettings, UpdateAppSettingsActions nS) =>
nS.appSettings;

View File

@ -1,15 +1,28 @@
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
final hasLoadedReducer = combineReducers<bool>([
TypedReducer<bool, TimeProgressListLoadedAction>(_setLoaded),
TypedReducer<bool, TimeProgressListNotLoadedAction>(_setUnloaded)
final hasProgressesLoadedReducer = combineReducers<bool>([
TypedReducer<bool, TimeProgressListLoadedAction>(_setProgressesLoaded),
TypedReducer<bool, TimeProgressListNotLoadedAction>(_setProgressesUnloaded)
]);
bool _setLoaded(bool hasLoaded, TimeProgressListLoadedAction action) {
bool _setProgressesLoaded(bool hasLoaded, TimeProgressListLoadedAction action) {
return true;
}
bool _setUnloaded(bool hasLoaded, TimeProgressListNotLoadedAction action) {
bool _setProgressesUnloaded(bool hasLoaded, TimeProgressListNotLoadedAction action) {
return false;
}
final hasSettingsLoadedReducer = combineReducers<bool>([
TypedReducer<bool, AppSettingsLoadedActions>(_setSettingsLoaded),
TypedReducer<bool, AppSettingsNotLoadedAction>(_setSettingsUnloaded)
]);
bool _setSettingsLoaded(bool hasLoaded, AppSettingsLoadedActions action) {
return true;
}
bool _setSettingsUnloaded(bool hasLoaded, AppSettingsNotLoadedAction action) {
return false;
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:time_progress_tracker/helper_functions.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_view.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
import 'package:time_progress_tracker/widgets/store_connectors/time_progress_list_store_connector.dart';
class ActiveTimeProgressesScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsStoreConnector(loadedBuilder: (context, settingsVm) {
return TimeProgressListStoreConnector(loadedBuilder: (context, tpListVm) {
List<TimeProgress> activeTpList =
selectActiveProgresses(tpListVm.tpList);
if (activeTpList.length < 1)
return Container(
padding: EdgeInsets.all(16),
child: Center(
child: PlatformText(
"You don't have any active time progress, that are tracked."),
),
);
return ProgressListView(
timeProgressList: activeTpList,
doneColor: settingsVm.appSettings.doneColor,
leftColor: settingsVm.appSettings.leftColor,
);
});
});
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:time_progress_tracker/screens/active_time_progresses_screen.dart';
import 'package:time_progress_tracker/screens/inactive_time_progresses_screen.dart';
import 'package:time_progress_tracker/screens/settings_screen.dart';
import 'package:time_progress_tracker/utils/color_utils.dart';
import 'package:time_progress_tracker/utils/constants.dart';
class DashboardScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
int _tabSelectedIndex = 0;
String title = txtActiveProgressesScreen;
Widget _renderTabScreen(int tabIndex) {
switch (tabIndex) {
case 1:
return InactiveTimeProgressesScreen();
case 2:
return SettingsScreen();
default:
return ActiveTimeProgressesScreen();
}
}
String getScreenTitle(int tabIndex) {
switch (tabIndex) {
case 1:
return txtInactiveProgressesScreen;
case 2:
return txtSettingsScreen;
default:
return txtActiveProgressesScreen;
}
}
@override
Widget build(BuildContext context) {
return PlatformScaffold(
appBar: PlatformAppBar(
title: Text(
title,
style: toolbarTextStyle,
),
cupertino: (_, __) => CupertinoNavigationBarData(
transitionBetweenRoutes: false,
),
),
material: (_, __) => MaterialScaffoldData(),
body: _renderTabScreen(_tabSelectedIndex),
bottomNavBar: PlatformNavBar(
currentIndex: _tabSelectedIndex,
itemChanged: (index) {
setState(() {
_tabSelectedIndex = index;
title = getScreenTitle(index);
});
},
backgroundColor: bottomTabsBackground,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.alarm, color: Colors.grey),
label: txtActiveProgressesScreen,
activeIcon: Icon(Icons.alarm, color: Colors.white),
),
BottomNavigationBarItem(
icon: Icon(Icons.alarm_off, color: Colors.grey),
label: txtInactiveProgressesScreen,
activeIcon: Icon(Icons.alarm_off, color: Colors.white),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings, color: Colors.grey),
label: txtSettingsScreen,
activeIcon: Icon(Icons.settings, color: Colors.white),
)
]),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/widgets/buttons/create_progress_button.dart';
import 'package:time_progress_tracker/widgets/home/home_bottom_navbar.dart';
import 'package:time_progress_tracker/widgets/home/tabs/home_active_progresses_tab.dart';
import 'package:time_progress_tracker/widgets/home/tabs/home_inactive_progresses_tab.dart';
import 'package:time_progress_tracker/widgets/home/tabs/home_settings_tab.dart';
class HomeScreen extends StatefulWidget {
static const routeName = "/home";
static const title = "Time Progress Tracker";
@override
State<StatefulWidget> createState() {
return _HomeScreenState();
}
}
class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;
final List<Widget> _children = [
HomeActiveProgressesTab(),
HomeInactiveProgressesTab(),
HomeSettingsTab(),
];
void onBottomTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(HomeScreen.title),
),
body: _children[_currentIndex],
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButton: _currentIndex != 2 ? CreateProgressButton() : null,
bottomNavigationBar: HomeBottomNavBar(
currentIndex: _currentIndex,
onTap: onBottomTabTapped,
),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:time_progress_tracker/helper_functions.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_view.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
import 'package:time_progress_tracker/widgets/store_connectors/time_progress_list_store_connector.dart';
class InactiveTimeProgressesScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsStoreConnector(loadedBuilder: (context, settingsVm) {
return TimeProgressListStoreConnector(loadedBuilder: (context, tpListVm) {
List<TimeProgress> activeTpList =
selectInactiveProgresses(tpListVm.tpList);
if (activeTpList.length < 1)
return Container(
padding: EdgeInsets.all(16),
child: Center(
child: PlatformText(
"You don't have any inactive time progress, that are tracked."),
),
);
return ProgressListView(
timeProgressList: activeTpList,
doneColor: settingsVm.appSettings.doneColor,
leftColor: settingsVm.appSettings.leftColor,
);
});
});
}
}

View File

@ -1,20 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_exceptions.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/screens/progress_dashboard_screen.dart';
import 'package:time_progress_tracker/widgets/app_drawer_widget.dart';
import 'package:time_progress_tracker/selectors/time_progress_selectors.dart';
import 'package:time_progress_tracker/widgets/progress_editor_widget.dart';
class ProgressCreationScreen extends StatefulWidget {
static const routeName = "/progress-creation";
final String appVersion;
ProgressCreationScreen({Key key, @required this.appVersion})
: super(key: key);
static const routeName = "/create-progress";
static const title = "Create Time Progress";
@override
State<StatefulWidget> createState() {
@ -23,130 +20,60 @@ class ProgressCreationScreen extends StatefulWidget {
}
class _ProgressCreationScreenState extends State<ProgressCreationScreen> {
final TextEditingController _nameController = TextEditingController();
DateTime pickedStartTime = DateTime.now();
DateTime pickedEndTime = DateTime(
DateTime.now().year + 1, DateTime.now().month, DateTime.now().day);
TimeProgress timeProgressToCreate;
bool _isProgressValid = false;
bool _validName = true;
Future<DateTime> _selectDate(
BuildContext context, DateTime initialDate) async {
return await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime(DateTime.now().year + 5));
}
void _createTimeProgress(BuildContext context) {
try {
TimeProgress tpToCreate =
TimeProgress(_nameController.text, pickedStartTime, pickedEndTime);
StoreProvider.of<AppState>(context)
.dispatch(AddTimeProgressAction(tpToCreate));
Navigator.pushNamed(context, ProgressDashboardScreen.routeName);
} on TimeProgressInvalidNameException catch (e) {
void initTimeProgress(TimeProgress timeProgress) {
if (timeProgressToCreate == null)
setState(() {
_validName = false;
timeProgressToCreate = timeProgress;
});
}
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
void onTimeProgressChanged(
TimeProgress newTimeProgress, bool isNewProgressValid) {
setState(() {
timeProgressToCreate = newTimeProgress;
_isProgressValid = isNewProgressValid;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Create Time Progress"),
),
drawer: AppDrawer(
appVersion: widget.appVersion,
title: Text(ProgressCreationScreen.title),
),
body: Container(
padding: EdgeInsets.all(8),
child: Column(
children: <Widget>[
Expanded(
flex: 1,
child: TextField(
controller: _nameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Progress Name",
errorText: _validName
? null
: "The Name of the Time Progress has to be set.",
),
),
),
Expanded(
child: Text("${_nameController.text}"),
),
Expanded(
flex: 1,
child: Row(
children: <Widget>[
Expanded(
flex: 5,
child: FlatButton(
color: Colors.blue,
child: Text(
"Start Date: ${pickedStartTime.toLocal().toString().split(" ")[0]}"),
onPressed: () async {
DateTime dt =
await _selectDate(context, pickedStartTime);
if (dt != null) {
setState(() {
pickedStartTime = dt;
});
}
},
),
),
Spacer(
flex: 1,
),
Expanded(
flex: 5,
child: FlatButton(
color: Colors.blue,
child: Text(
"End Date: ${pickedEndTime.toLocal().toString().split(" ")[0]}"),
onPressed: () async {
DateTime dt = await _selectDate(context, pickedEndTime);
if (dt != null) {
setState(() {
pickedEndTime = dt;
});
}
},
),
),
],
),
),
Spacer(
flex: 5,
)
],
),
padding: EdgeInsets.all(12),
child: StoreConnector<AppState, _ViewModel>(
onInit: loadSettingsIfUnloaded,
converter: (store) => _ViewModel.create(store),
builder: (context, _ViewModel viewModel) {
initTimeProgress(viewModel.defaultDurationProgress);
return ProgressEditorWidget(
timeProgress: timeProgressToCreate,
onTimeProgressChanged: onTimeProgressChanged,
);
}),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: <Widget>[
Expanded(
child: FloatingActionButton(
heroTag: "createTimeProgressBTN",
child: Icon(Icons.save),
onPressed: () {
_createTimeProgress(context);
},
child: StoreConnector<AppState, _ViewModel>(
onInit: loadSettingsIfUnloaded,
converter: (store) => _ViewModel.create(store),
builder: (context, _ViewModel vm) => FloatingActionButton(
heroTag: "createTimeProgressBTN",
child: Icon(Icons.save),
onPressed: _isProgressValid
? () {
vm.onAddTimeProgress(timeProgressToCreate);
Navigator.pop(context);
}
: null,
),
),
),
Expanded(
@ -154,7 +81,7 @@ class _ProgressCreationScreenState extends State<ProgressCreationScreen> {
heroTag: "cancelTimeProgressCreationBTN",
child: Icon(Icons.cancel),
onPressed: () {
Navigator.pushNamed(context, ProgressDashboardScreen.routeName);
Navigator.pop(context);
},
),
)
@ -163,3 +90,27 @@ class _ProgressCreationScreenState extends State<ProgressCreationScreen> {
);
}
}
class _ViewModel {
final TimeProgress defaultDurationProgress;
final void Function(TimeProgress) onAddTimeProgress;
_ViewModel({
@required this.defaultDurationProgress,
@required this.onAddTimeProgress,
});
factory _ViewModel.create(Store<AppState> store) {
AppSettings settings = appSettingsSelector(store.state);
_onAddTimeProgress(TimeProgress tp) {
if (TimeProgress.isValid(tp)) store.dispatch(AddTimeProgressAction(tp));
}
return _ViewModel(
defaultDurationProgress:
TimeProgress.defaultFromDuration(settings.duration),
onAddTimeProgress: _onAddTimeProgress,
);
}
}

View File

@ -1,181 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/screens/progress_creation_screen.dart';
import 'package:time_progress_tracker/screens/progress_detail_screen.dart';
import 'package:time_progress_tracker/selectors/time_progress_selectors.dart';
import 'package:time_progress_tracker/widgets/app_drawer_widget.dart';
class ProgressDashboardScreen extends StatelessWidget {
static const routeName = "/progress-dashboard";
static const title = "Time Progress Dashboard";
final String appVersion;
ProgressDashboardScreen({
Key key,
@required this.appVersion,
}) : super(key: key);
@override
Widget build(BuildContext context) {
AppBar appBar = AppBar(
title: Text(title),
);
return Scaffold(
appBar: appBar,
drawer: AppDrawer(
appVersion: appVersion,
),
body: StoreConnector(
converter: _ViewModel.fromStore,
onInit: loadTimeProgressListIfUnloaded,
builder: (BuildContext context, _ViewModel vm) {
if (!vm.hasLoaded) {
return Center(
child: CircularProgressIndicator(),
);
}
List<Widget> startedProgressesTileList = List<Widget>();
if (vm.hasStartedProgresses) {
for (TimeProgress tp in vm.startedTimeProgreses) {
startedProgressesTileList.add(
Card(
child: ListTile(
title: Text(tp.name),
subtitle: LinearPercentIndicator(
center: Text("${(tp.percentDone() * 100).floor()} %"),
percent: tp.percentDone(),
progressColor: Colors.green,
backgroundColor: Colors.red,
lineHeight: 20,
),
onTap: () {
Navigator.pushNamed(
context, ProgressDetailScreen.routeName,
arguments: ProgressDetailScreenArguments(tp.id));
},
),
),
);
}
}
List<Widget> futureProgressesTileList = List<Widget>();
if (vm.hasFutureProgresses) {
for (TimeProgress tp in vm.futureTimeProgresses) {
futureProgressesTileList.add(
Card(
child: ListTile(
title: Text(tp.name),
subtitle: Text(
"Starts in ${tp.startTime.difference(DateTime.now()).inDays} Days."),
onTap: () {
Navigator.pushNamed(
context, ProgressDetailScreen.routeName,
arguments: ProgressDetailScreenArguments(tp.id));
},
),
),
);
}
}
double dividerHeight = 1;
double screenHeight = MediaQuery.of(context).size.height -
appBar.preferredSize.height -
24 -
dividerHeight; //Divider
List<Widget> columnChildren = List<Widget>();
int tpCount =
vm.startedTimeProgreses.length + vm.futureTimeProgresses.length;
if (vm.hasStartedProgresses) {
columnChildren.add(Container(
height: vm.hasFutureProgresses
? (screenHeight / tpCount) * vm.startedTimeProgreses.length
: screenHeight,
child: ListView(
padding: EdgeInsets.all(8),
children: startedProgressesTileList,
),
));
}
if (vm.hasStartedProgresses && vm.hasFutureProgresses) {
columnChildren.add(Divider(
height: dividerHeight,
));
}
if (vm.hasFutureProgresses) {
columnChildren.add(Container(
height: vm.hasStartedProgresses
? (screenHeight / tpCount) * vm.futureTimeProgresses.length
: screenHeight,
child: ListView(
padding: EdgeInsets.all(8),
children: futureProgressesTileList,
),
));
}
if (!vm.hasStartedProgresses && !vm.hasFutureProgresses) {
columnChildren.add(Container(
margin: EdgeInsets.all(16),
child: Center(
child: Text("You don't have any tracked Progress."),
),
));
}
return Column(
children: columnChildren,
);
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButton: FloatingActionButton(
heroTag: "createProgressBTN",
child: Icon(Icons.add),
onPressed: () {
Navigator.pushNamed(context, ProgressCreationScreen.routeName);
},
),
);
}
}
class _ViewModel {
final List<TimeProgress> startedTimeProgreses;
final bool hasStartedProgresses;
final List<TimeProgress> futureTimeProgresses;
final bool hasFutureProgresses;
final bool hasLoaded;
_ViewModel({
@required this.startedTimeProgreses,
@required this.hasStartedProgresses,
@required this.futureTimeProgresses,
@required this.hasFutureProgresses,
@required this.hasLoaded,
});
static _ViewModel fromStore(Store<AppState> store) {
List<TimeProgress> startedTPList =
startedTimeProgressesSelector(store.state);
List<TimeProgress> furtureTPList =
futureTimeProgressesSelector(store.state);
return _ViewModel(
startedTimeProgreses: startedTPList,
hasStartedProgresses: startedTPList.length > 0,
futureTimeProgresses: furtureTPList,
hasFutureProgresses: furtureTPList.length > 0,
hasLoaded: store.state.hasLoaded,
);
}
}

View File

@ -1,19 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_exceptions.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/screens/progress_dashboard_screen.dart';
import 'package:time_progress_tracker/selectors/time_progress_selectors.dart';
import 'package:time_progress_tracker/widgets/app_drawer_widget.dart';
import 'package:time_progress_tracker/widgets/app_yes_no_dialog_widget.dart';
import 'package:time_progress_tracker/widgets/progress_detail_widgets/progress_detail_circular_percent_widget.dart';
import 'package:time_progress_tracker/widgets/progress_detail_widgets/progress_detail_edit_dates_row_widget.dart';
import 'package:time_progress_tracker/widgets/progress_detail_widgets/progress_detail_fab_editing_row_widget.dart';
import 'package:time_progress_tracker/widgets/progress_detail_widgets/progress_detail_fab_row_widget.dart';
import 'package:time_progress_tracker/widgets/progress_detail_widgets/progress_detail_linear_percent_widget.dart';
import 'package:time_progress_tracker/screens/home_screen.dart';
import 'package:time_progress_tracker/widgets/detail_screen_floating_action_buttons.dart';
import 'package:time_progress_tracker/widgets/progress_editor_widget.dart';
import 'package:time_progress_tracker/widgets/progress_view_widget.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
import 'package:time_progress_tracker/widgets/store_connectors/time_progress_store_connector.dart';
class ProgressDetailScreenArguments {
final String id;
@ -22,14 +14,8 @@ class ProgressDetailScreenArguments {
}
class ProgressDetailScreen extends StatefulWidget {
static const routeName = "/progress-detail";
final String appVersion;
ProgressDetailScreen({
Key key,
@required this.appVersion,
}) : super(key: key);
static const routeName = "/progress";
static const title = "Progress View";
@override
State<StatefulWidget> createState() {
@ -38,258 +24,106 @@ class ProgressDetailScreen extends StatefulWidget {
}
class _ProgressDetailScreenState extends State<ProgressDetailScreen> {
bool _isBeingEdited = false;
final TextEditingController _nameController = TextEditingController();
bool _editMode = false, _isEditedProgressValid = false;
TimeProgress _editedProgress, _originalProgress;
TimeProgress _editedProgress = TimeProgress.initialDefault();
bool _validName = true;
void _onStartDateChanged(DateTime picked) {
if (picked != null) {
setState(() {
_editedProgress = _editedProgress.copyWith(startTime: picked);
});
void _initEditedProgress(TimeProgress tp) {
if (_editedProgress == null) {
_editedProgress = tp;
_originalProgress = tp;
}
}
void _onEndDateChanged(DateTime picked) {
if (picked != null) {
setState(() {
_editedProgress = _editedProgress.copyWith(endTime: picked);
});
}
}
void _onSaveTimeProgress(Store<AppState> store, id) {
store.dispatch(UpdateTimeProgressAction(id, _editedProgress));
void _onEditedProgressChanged(
TimeProgress newProgress, bool isNewProgressValid) {
setState(() {
_isBeingEdited = false;
_editedProgress = newProgress;
_isEditedProgressValid = isNewProgressValid;
});
}
void _showCancelEditTimeProgressDialog(AppState state, id) {
TimeProgress originalTp = timeProgressByIdSelector(state, id);
if (originalTp != _editedProgress) {
String originalName = timeProgressByIdSelector(state, id).name;
showDialog(
context: context,
builder: (_) => AppYesNoDialog(
titleText: "Cancel Editing of $originalName",
contentText:
"Are you sure that you want to discard the changes done to $originalName",
onYesPressed: _onCancelEditTimeProgress,
onNoPressed: _onCloseDialog,
),
);
} else {
setState(() {
_isBeingEdited = false;
});
}
}
void _onCancelEditTimeProgress() {
void _switchEditMode(bool newMode) {
setState(() {
_isBeingEdited = false;
_editMode = newMode;
});
Navigator.pop(context);
}
void _onEditTimeProgress(Store<AppState> store, id) {
void _cancelEditMode() {
setState(() {
_isBeingEdited = true;
_editedProgress = timeProgressByIdSelector(store.state, id);
_nameController.text = _editedProgress.name;
_editMode = false;
_editedProgress = _originalProgress;
});
}
void _showDeleteTimeProgressDialog(Store<AppState> store, id) {
showDialog(
context: context,
builder: (_) => AppYesNoDialog(
titleText: "Delete ${timeProgressByIdSelector(store.state, id).name}",
contentText: "Are you sure you want to delete this time progress?",
onYesPressed: () => _onDeleteTimeProgress(store, id),
onNoPressed: _onCloseDialog,
),
);
}
void _onDeleteTimeProgress(Store<AppState> store, String id) {
store.dispatch(DeleteTimeProgressAction(id));
Navigator.popAndPushNamed(context, ProgressDashboardScreen.routeName);
}
void _onCloseDialog() {
Navigator.pop(context);
}
@override
void initState() {
super.initState();
_nameController.addListener(() {
try {
TimeProgress editedProgress =
_editedProgress.copyWith(name: _nameController.text);
setState(() {
_editedProgress = editedProgress;
_validName = true;
});
} on TimeProgressInvalidNameException catch (e) {
setState(() {
_validName = false;
});
}
});
List<Widget> _renderColumnChildren(
SettingsViewModel settingsVm, TimeProgressViewModel tpVm) {
List<Widget> columnChildren = [
Expanded(
child: ProgressViewWidget(
timeProgress: _editMode ? _editedProgress : tpVm.tp,
doneColor: settingsVm.appSettings.doneColor,
leftColor: settingsVm.appSettings.leftColor,
))
];
if (_editMode)
columnChildren.add(Expanded(
child: ProgressEditorWidget(
timeProgress: _editedProgress,
onTimeProgressChanged: _onEditedProgressChanged,
)));
return columnChildren;
}
@override
Widget build(BuildContext context) {
final ProgressDetailScreenArguments args =
ModalRoute.of(context).settings.arguments;
final Store<AppState> store = StoreProvider.of<AppState>(context);
return Scaffold(
appBar: AppBar(
title: Text("Progress"),
title: Text(ProgressDetailScreen.title),
),
drawer: AppDrawer(
appVersion: widget.appVersion,
),
body: Container(
margin: EdgeInsets.all(8),
child: StoreConnector(
converter: (Store<AppState> store) =>
_ViewModel.fromStoreAndArg(store, args),
onInit: loadTimeProgressListIfUnloaded,
builder: (BuildContext context, _ViewModel vm) {
return Column(
children: <Widget>[
Expanded(
flex: 1,
child: _isBeingEdited
? TextField(
controller: _nameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Progress Name",
errorText: _validName
? null
: "The Name of the Time Progress has to be set.",
),
)
: vm.hasProgressStarted
? FittedBox(
fit: BoxFit.fitWidth,
child: Text(
vm.timeProgress.name,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
)
: Center(
child: FittedBox(
fit: BoxFit.fitWidth,
child: Text(
vm.timeProgress.name,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
),
),
vm.hasProgressStarted
? Expanded(
flex: 2,
child: ProgressDetailCircularPercent(
percentDone: _isBeingEdited
? _editedProgress.percentDone()
: vm.timeProgress.percentDone(),
),
)
: Expanded(
flex: 2,
child: Text(
"Starts in ${vm.timeProgress.startTime.difference(DateTime.now()).inDays} Days."),
),
vm.hasProgressStarted
? Expanded(
flex: 1,
child: ProgressDetailLinearPercent(
timeProgress: _isBeingEdited
? _editedProgress
: vm.timeProgress,
),
)
: Spacer(
flex: 1,
),
Expanded(
flex: 1,
child: Text(
"${_isBeingEdited ? _editedProgress.allDays() : vm.timeProgress.allDays()} Days"),
),
this._isBeingEdited
? Expanded(
flex: 1,
child: ProgressDetailEditDatesRow(
startTime: _editedProgress.startTime,
endTime: _editedProgress.endTime,
onStartTimeChanged: _onStartDateChanged,
onEndTimeChanged: _onEndDateChanged,
),
)
: Spacer(flex: 1),
Spacer(flex: 1)
],
);
},
),
body: SettingsStoreConnector(
loadedBuilder: (context, settingsVm) {
return TimeProgressStoreConnector(
timeProgressId: args.id,
loadedBuilder: (context, tpVm) {
_initEditedProgress(tpVm.tp);
return Container(
margin: EdgeInsets.all(8),
child: Column(
children: _renderColumnChildren(settingsVm, tpVm),
));
},
);
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _isBeingEdited
? ProgressDetailFabEditingRow(
onSave: () =>
_validName ? _onSaveTimeProgress(store, args.id) : null,
onCancelEdit: () =>
_showCancelEditTimeProgressDialog(store.state, args.id),
)
: ProgressDetailFabRow(
onEdit: () => _onEditTimeProgress(store, args.id),
onDelete: () => _showDeleteTimeProgressDialog(store, args.id),
),
floatingActionButton: TimeProgressStoreConnector(
timeProgressId: args.id,
loadedBuilder: (context, tpVm) {
void _saveEditedProgress() {
tpVm.updateTimeProgress(_editedProgress);
_switchEditMode(false);
}
void _deleteTimeProgress() {
tpVm.deleteTimeProgress();
Navigator.popUntil(
context, ModalRoute.withName(HomeScreen.routeName));
}
return DetailScreenFloatingActionButtons(
editMode: _editMode,
originalProgress: tpVm.tp,
editedProgress: _editedProgress,
isEditedProgressValid: _isEditedProgressValid,
onEditProgress: () => _switchEditMode(true),
onSaveEditedProgress: _saveEditedProgress,
onCancelEditProgress: _cancelEditMode,
onDeleteProgress: _deleteTimeProgress);
},
),
);
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
}
class _ViewModel {
final TimeProgress timeProgress;
final bool hasProgressStarted;
_ViewModel({
@required this.timeProgress,
@required this.hasProgressStarted,
});
static _ViewModel fromStoreAndArg(
Store<AppState> store, ProgressDetailScreenArguments args) {
TimeProgress tp = timeProgressByIdSelector(store.state, args.id);
return _ViewModel(
timeProgress: tp,
hasProgressStarted: DateTime.now().millisecondsSinceEpoch >
tp.startTime.millisecondsSinceEpoch);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:time_progress_tracker/app.dart';
import 'package:time_progress_tracker/widgets/buttons/color_picker_btn.dart';
import 'package:time_progress_tracker/widgets/home/tabs/settings/duration_settings_widget.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
class SettingsScreen extends StatelessWidget {
Widget _renderColorSettings(
BuildContext context, SettingsViewModel settingsVm) {
ThemeData appTheme = Theme.of(context);
return Expanded(
child: Column(
children: [
Expanded(
child: PlatformText(
"Color Settings",
style: appTheme.primaryTextTheme.caption,
)),
Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 5),
child: ColorPickerButton(
title: "Done Color",
dialogTitle: "Select Done Color",
selectedColor: settingsVm.appSettings.doneColor,
onColorPicked: settingsVm.updateDoneColor,
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 5),
child: ColorPickerButton(
title: "Left Color",
dialogTitle: "Select Left Color",
selectedColor: settingsVm.appSettings.leftColor,
onColorPicked: settingsVm.updateLeftColor,
),
),
)
],
)
],
),
);
}
@override
Widget build(BuildContext context) {
return SettingsStoreConnector(loadedBuilder: (context, settingsVm) {
return Container(
padding: EdgeInsets.all(16),
child: Center(
child: Column(
children: [
Expanded(
child: _renderColorSettings(context, settingsVm),
),
Expanded(
child: DurationSettingsWidget(
duration: settingsVm.appSettings.duration,
updateDuration: settingsVm.updateDuration,
),
),
Spacer(),
Expanded(
child: PlatformButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: TimeProgressTrackerApp.name,
applicationVersion: "Beta",
applicationLegalese:
'\u00a9Andreas Fahrecker 2020-2021');
},
child: Text("About"),
),
),
],
),
),
);
});
}
}

View File

@ -1,15 +1,35 @@
import 'dart:ui';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
List<TimeProgress> timeProgressListSelector(AppState state) =>
state.timeProgressList;
List<TimeProgress> startedTimeProgressesSelector(AppState state) =>
state.timeProgressList
.where((timeProgress) =>
DateTime.now().millisecondsSinceEpoch >=
timeProgress.startTime.millisecondsSinceEpoch)
.toList();
List<TimeProgress> activeTimeProgressesSelector(AppState state) {
return state.timeProgressList
.where((timeProgress) =>
timeProgress.hasStarted() && !timeProgress.hasEnded())
.toList();
}
List<TimeProgress> inactiveTimeProgressesSelector(AppState state) {
return state.timeProgressList
.where((timeProgress) =>
!timeProgress.hasStarted() || timeProgress.hasEnded())
.toList();
}
@Deprecated("use active TimeProgresses Selector instead.")
List<TimeProgress> currentTimeProgressSelector(AppState state) {
int currentTime = DateTime.now().millisecondsSinceEpoch;
return state.timeProgressList
.where((tp) =>
currentTime >= tp.startTime.millisecondsSinceEpoch &&
tp.endTime.millisecondsSinceEpoch >= currentTime)
.toList();
}
List<TimeProgress> futureTimeProgressesSelector(AppState state) =>
state.timeProgressList
@ -18,8 +38,21 @@ List<TimeProgress> futureTimeProgressesSelector(AppState state) =>
timeProgress.startTime.millisecondsSinceEpoch)
.toList();
List<TimeProgress> pastTimeProgressesSelector(AppState state) =>
state.timeProgressList
.where((tp) =>
tp.endTime.millisecondsSinceEpoch <
DateTime.now().millisecondsSinceEpoch)
.toList();
TimeProgress timeProgressByIdSelector(AppState state, String id) {
if (state.timeProgressList.length < 1) return null;
return state.timeProgressList
.firstWhere((timeProgress) => timeProgress.id == id);
.firstWhere((timeProgress) => timeProgress.id == id, orElse: () => null);
}
AppSettings appSettingsSelector(AppState state) {
return state.appSettings;
}
Color doneColorSelector(AppState state) => state.appSettings.doneColor;

View File

@ -0,0 +1,20 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
final ThemeData materialThemeData = ThemeData(
primarySwatch: Colors.indigo,
scaffoldBackgroundColor: Colors.white,
accentColor: Colors.indigo,
appBarTheme: AppBarTheme(color: Colors.indigo.shade600),
primaryColor: Colors.indigo,
secondaryHeaderColor: Colors.indigo,
canvasColor: Colors.indigo,
backgroundColor: Colors.red,
);
final CupertinoThemeData cupertinoThemeData = CupertinoThemeData(
primaryColor: Colors.indigo,
barBackgroundColor: Colors.indigo,
scaffoldBackgroundColor: Colors.white,
);
final toolbarTextStyle = TextStyle(color: Colors.white, fontSize: 16);
final bottomTabsBackground = Colors.indigoAccent;

3
lib/utils/constants.dart Normal file
View File

@ -0,0 +1,3 @@
const txtActiveProgressesScreen = "Active Progresses";
const txtInactiveProgressesScreen = "Inactive Progresses";
const txtSettingsScreen = "Settings";

View File

@ -1,127 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/app.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/screens/progress_dashboard_screen.dart';
import 'package:time_progress_tracker/screens/progress_detail_screen.dart';
import 'package:time_progress_tracker/selectors/time_progress_selectors.dart';
class AppDrawer extends StatelessWidget {
final String appVersion;
AppDrawer({
Key key,
@required this.appVersion,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer(
child: StoreConnector(
converter: _ViewModel.fromStore,
onInit: loadTimeProgressListIfUnloaded,
builder: (context, _ViewModel vm) {
if (!vm.hasLoaded) {
return Center(
child: CircularProgressIndicator(),
);
}
List<Widget> drawerTileList = List<Widget>();
drawerTileList.add(DrawerHeader(
child: Text(TimeProgressTrackerApp.name),
decoration: BoxDecoration(color: Colors.blue),
margin: EdgeInsets.zero,
));
drawerTileList.add(Container(
color: Colors.lightBlue,
margin: EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(ProgressDashboardScreen.title),
trailing: Icon(Icons.dashboard),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, ProgressDashboardScreen.routeName);
},
),
));
if (vm.startedTimeProgresses.length > 0) {
for (TimeProgress tp in vm.startedTimeProgresses) {
drawerTileList.add(ListTile(
title: Text(tp.name),
trailing: CircularPercentIndicator(
percent: tp.percentDone(),
radius: 40,
progressColor: Colors.green,
backgroundColor: Colors.red,
center: FittedBox(
fit: BoxFit.scaleDown,
child:
Text((tp.percentDone() * 100).floor().toString() + "%"),
),
),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(
context,
ProgressDetailScreen.routeName,
arguments: ProgressDetailScreenArguments(tp.id),
);
},
));
if (vm.startedTimeProgresses.last != tp) {
drawerTileList.add(Divider(
color: Colors.black12,
));
}
}
} else {
drawerTileList.add(ListTile(
title: Text("You don't have any tracked time progress."),
));
}
drawerTileList.add(Divider(
color: Colors.black38,
));
drawerTileList.add(Container(
margin: EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text("About"),
onTap: () {
showAboutDialog(
context: context,
applicationName: TimeProgressTrackerApp.name,
applicationVersion: " Version $appVersion",
applicationLegalese: '\u00a9Andreas Fahrecker 2020');
},
),
));
return ListView(
children: drawerTileList,
);
},
),
);
}
}
class _ViewModel {
final List<TimeProgress> startedTimeProgresses;
final bool hasLoaded;
_ViewModel({
@required this.startedTimeProgresses,
@required this.hasLoaded,
});
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(
startedTimeProgresses: startedTimeProgressesSelector(store.state),
hasLoaded: store.state.hasLoaded,
);
}
}

View File

@ -4,14 +4,12 @@ class AppYesNoDialog extends StatelessWidget {
final String titleText;
final String contentText;
final void Function() onYesPressed;
final void Function() onNoPressed;
AppYesNoDialog({
Key key,
@required this.titleText,
@required this.contentText,
@required this.onYesPressed,
@required this.onNoPressed,
}) : super(key: key);
@override
@ -26,7 +24,9 @@ class AppYesNoDialog extends StatelessWidget {
),
FlatButton(
child: Text("No"),
onPressed: onNoPressed,
onPressed: () {
Navigator.pop(context);
},
)
],
);

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:time_progress_tracker/helper_functions.dart';
class ColorPickerButton extends StatelessWidget {
final String title, dialogTitle;
final Color selectedColor;
final void Function(Color) onColorPicked;
ColorPickerButton({
@required this.title,
@required this.dialogTitle,
@required this.selectedColor,
@required this.onColorPicked,
});
@override
Widget build(BuildContext context) {
ThemeData appTheme = Theme.of(context);
return TextButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(dialogTitle),
content: SingleChildScrollView(
child: BlockPicker(
pickerColor: selectedColor,
onColorChanged: onColorPicked,
),
),
);
},
);
},
child: Text(title),
style: TextButton.styleFrom(
primary: useBrightBackground(selectedColor)
? appTheme.primaryTextTheme.button.color
: appTheme.textTheme.button.color,
backgroundColor: selectedColor,
),
);
}
}

View File

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/screens/progress_creation_screen.dart';
class CreateProgressButton extends StatelessWidget {
final String _heroTag = "createProgressBTN";
@override
Widget build(BuildContext context) {
void _onButtonPressed() =>
Navigator.pushNamed(context, ProgressCreationScreen.routeName);
return FloatingActionButton(
heroTag: _heroTag,
child: Icon(Icons.add),
onPressed: _onButtonPressed,
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class DatePickerBtn extends StatelessWidget {
final String leadingString;
final DateTime pickedDate;
final void Function(DateTime) onDatePicked;
DatePickerBtn({
@required this.leadingString,
@required this.pickedDate,
@required this.onDatePicked,
}) : super();
void _onButtonPressed(BuildContext context) async {
onDatePicked(await showDatePicker(
context: context,
initialDate: pickedDate,
firstDate: DateTime(1900),
lastDate: DateTime(2100),
));
}
@override
Widget build(BuildContext context) {
ThemeData appTheme = Theme.of(context);
return TextButton(
onPressed: () => _onButtonPressed(context),
child: Text(
"$leadingString ${pickedDate.toLocal().toString().split(" ")[0]}"),
style: TextButton.styleFrom(
primary: appTheme.primaryTextTheme.button.color,
backgroundColor: appTheme.accentColor,
),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_picker/flutter_picker.dart';
class SelectDurationBtn extends StatelessWidget {
final Duration duration;
final void Function(Duration) updateDuration;
SelectDurationBtn({
@required this.duration,
@required this.updateDuration,
});
void _onPickerConfirm(Picker picker, List<int> values) {
int years = values[0], months = values[1], days = values[2];
days = (years * 365) + (months * 31) + days;
Duration newDuration = Duration(days: days);
updateDuration(newDuration);
}
void _onButtonPressed(BuildContext context, ThemeData appTheme) => Picker(
adapter: NumberPickerAdapter(data: [
const NumberPickerColumn(begin: 0, end: 999, suffix: Text(" Y")),
const NumberPickerColumn(begin: 0, end: 11, suffix: Text(" M")),
const NumberPickerColumn(begin: 0, end: 31, suffix: Text(" D")),
]),
hideHeader: false,
title: const Text("Default Duration"),
selectedTextStyle: TextStyle(color: appTheme.accentColor),
onConfirm: _onPickerConfirm)
.showModal(context);
@override
Widget build(BuildContext context) {
ThemeData appTheme = Theme.of(context);
int years = duration.inDays ~/ 365;
int months = (duration.inDays - (365 * years)) ~/ 30;
int days = duration.inDays - (365 * years) - (30 * months);
return TextButton(
onPressed: () => _onButtonPressed(context, appTheme),
child: Text("$years Years $months Months $days Days"),
style: TextButton.styleFrom(
primary: appTheme.primaryTextTheme.button.color,
backgroundColor: appTheme.accentColor,
));
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/app_yes_no_dialog_widget.dart';
class DetailScreenFloatingActionButtons extends StatelessWidget {
final bool editMode, isEditedProgressValid;
final TimeProgress originalProgress, editedProgress;
final void Function() onEditProgress,
onSaveEditedProgress,
onCancelEditProgress,
onDeleteProgress;
DetailScreenFloatingActionButtons({
@required this.editMode,
@required this.originalProgress,
@required this.editedProgress,
@required this.isEditedProgressValid,
@required this.onEditProgress,
@required this.onSaveEditedProgress,
@required this.onCancelEditProgress,
@required this.onDeleteProgress,
});
@override
Widget build(BuildContext context) {
final ThemeData appTheme = Theme.of(context);
void _onCancelEditTimeProgressBTN() {
if (originalProgress == editedProgress)
onCancelEditProgress();
else {
showDialog(
context: context,
builder: (_) => AppYesNoDialog(
titleText: "Cancel Editing of ${originalProgress.name}",
contentText:
"Are you sure that you want to discard the changes done to ${originalProgress.name}",
onYesPressed: () {
onCancelEditProgress();
Navigator.pop(context);
},
),
);
}
}
void _onDeleteTimeProgressBTN() {
showDialog(
context: context,
builder: (_) => AppYesNoDialog(
titleText: "Delete ${originalProgress.name}",
contentText: "Are you sure you want to delete this time progress?",
onYesPressed: onDeleteProgress,
),
);
}
return Row(
children: [
Expanded(
child: FloatingActionButton(
heroTag:
editMode ? "saveEditedTimeProgressBTN" : "editTimeProgressBTN",
child: editMode ? Icon(Icons.save) : Icon(Icons.edit),
backgroundColor: editMode ? Colors.green : appTheme.accentColor,
onPressed: editMode
? isEditedProgressValid
? onSaveEditedProgress
: null
: onEditProgress,
),
),
Expanded(
child: FloatingActionButton(
heroTag: editMode
? "cancelEditTimeProgressBTN"
: "deleteTimeProgressBTN",
child: editMode ? Icon(Icons.cancel) : Icon(Icons.delete),
backgroundColor: Colors.red,
onPressed: editMode
? _onCancelEditTimeProgressBTN
: _onDeleteTimeProgressBTN,
),
),
],
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
class HomeBottomNavBar extends StatelessWidget {
final int currentIndex;
final Function onTap;
HomeBottomNavBar({
Key key,
@required this.currentIndex,
@required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
ThemeData appTheme = Theme.of(context);
return BottomNavigationBar(
onTap: onTap,
currentIndex: currentIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(
Icons.alarm,
color: appTheme.primaryColor,
),
label: "Active Progresses",
),
BottomNavigationBarItem(
icon: new Icon(
Icons.alarm_off,
color: appTheme.primaryColor,
),
label: "Inactive Progresses",
),
BottomNavigationBarItem(
icon: new Icon(
Icons.settings,
color: appTheme.primaryColor,
),
label: "Settings",
)
],
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/helper_functions.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_view.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
import 'package:time_progress_tracker/widgets/store_connectors/time_progress_list_store_connector.dart';
class HomeActiveProgressesTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsStoreConnector(
loadedBuilder: (context, settingsVm) {
return TimeProgressListStoreConnector(
loadedBuilder: (context, tpListVm) {
List<TimeProgress> activeTpList =
selectActiveProgresses(tpListVm.tpList);
if (activeTpList.length < 1)
return Container(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
"You don't have any active time progress, that are tracked."),
),
);
return ProgressListView(
timeProgressList: activeTpList,
doneColor: settingsVm.appSettings.doneColor,
leftColor: settingsVm.appSettings.leftColor,
);
},
);
},
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/helper_functions.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_view.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
import 'package:time_progress_tracker/widgets/store_connectors/time_progress_list_store_connector.dart';
class HomeInactiveProgressesTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsStoreConnector(
loadedBuilder: (context, settingsVm) {
return TimeProgressListStoreConnector(
loadedBuilder: (context, tpListVm) {
List<TimeProgress> inactiveTpList =
selectInactiveProgresses(tpListVm.tpList);
if (inactiveTpList.length < 1)
return Container(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
"You don't have any currently inactive time progresses, that are tracked."),
),
);
return ProgressListView(
timeProgressList: inactiveTpList,
doneColor: settingsVm.appSettings.doneColor,
leftColor: settingsVm.appSettings.leftColor,
);
},
);
},
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/app.dart';
import 'package:time_progress_tracker/widgets/home/tabs/settings/color_settings_widget.dart';
import 'package:time_progress_tracker/widgets/home/tabs/settings/duration_settings_widget.dart';
import 'package:time_progress_tracker/widgets/store_connectors/settings_store_connector.dart';
class HomeSettingsTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsStoreConnector(
loadedBuilder: (context, settingsVm) {
return Container(
padding: EdgeInsets.all(16),
child: Center(
child: Column(
children: [
Expanded(
child: ColorSettingsWidget(
doneColor: settingsVm.appSettings.doneColor,
leftColor: settingsVm.appSettings.leftColor,
updateDoneColor: settingsVm.updateDoneColor,
updateLeftColor: settingsVm.updateLeftColor,
),
),
Expanded(
child: DurationSettingsWidget(
duration: settingsVm.appSettings.duration,
updateDuration: settingsVm.updateDuration,
),
),
Spacer(),
Expanded(
child: TextButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: TimeProgressTrackerApp.name,
applicationVersion: "Beta",
applicationLegalese:
'\u00a9Andreas Fahrecker 2020-2021');
},
child: Text("About"),
),
),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/widgets/buttons/color_picker_btn.dart';
class ColorSettingsWidget extends StatelessWidget {
final Color doneColor, leftColor;
final void Function(Color) updateDoneColor, updateLeftColor;
ColorSettingsWidget({
@required this.doneColor,
@required this.leftColor,
@required this.updateDoneColor,
@required this.updateLeftColor,
});
@override
Widget build(BuildContext context) {
ThemeData appTheme = Theme.of(context);
return Column(
children: [
Expanded(
child: Text(
"Color Settings",
style: appTheme.textTheme.headline6,
),
),
Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 5),
child: ColorPickerButton(
title: "Done Color",
dialogTitle: "Select Done Color",
selectedColor: doneColor,
onColorPicked: updateDoneColor,
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 5),
child: ColorPickerButton(
title: "Left Color",
dialogTitle: "Select Left Color",
selectedColor: leftColor,
onColorPicked: updateLeftColor,
),
),
),
],
)
],
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/widgets/buttons/select_duration_btn.dart';
class DurationSettingsWidget extends StatelessWidget {
final Duration duration;
final void Function(Duration) updateDuration;
DurationSettingsWidget({
@required this.duration,
@required this.updateDuration,
});
@override
Widget build(BuildContext context) {
ThemeData appTheme = Theme.of(context);
int years = duration.inDays ~/ 365;
int months = (duration.inDays - (365 * years)) ~/ 30;
int days = duration.inDays - (365 * years) - (30 * months);
return Column(
children: [
Expanded(
child: Text(
"Duration Settings",
style: appTheme.textTheme.headline6,
),
),
Row(
children: [
Expanded(
child: SelectDurationBtn(
duration: duration,
updateDuration: updateDuration,
),
)
],
)
],
);
}
}

View File

@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
class ProgressDetailCircularPercent extends StatelessWidget {
final double percentDone;
ProgressDetailCircularPercent({Key key, @required this.percentDone})
: super(key: key);
@override
Widget build(BuildContext context) {
return CircularPercentIndicator(
radius: 100,
lineWidth: 10,
percent: percentDone,
progressColor: Colors.green,
backgroundColor: Colors.red,
center: Text("${(percentDone * 100).floor()} %"),
);
}
}

View File

@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/widgets/progress_detail_widgets/progress_detail_select_date_btn_widget.dart';
class ProgressDetailEditDatesRow extends StatelessWidget {
final DateTime startTime;
final DateTime endTime;
final void Function(DateTime) onStartTimeChanged;
final void Function(DateTime) onEndTimeChanged;
ProgressDetailEditDatesRow({
Key key,
@required this.startTime,
@required this.endTime,
@required this.onStartTimeChanged,
@required this.onEndTimeChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
flex: 5,
child: ProgressDetailSelectDateButton(
leadingString: "Start Date:",
selectedDate: startTime,
onDateSelected: onStartTimeChanged,
),
),
Spacer(
flex: 1,
),
Expanded(
flex: 5,
child: ProgressDetailSelectDateButton(
leadingString: "End Date:",
selectedDate: endTime,
onDateSelected: onEndTimeChanged,
),
)
],
);
}
}

View File

@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
class ProgressDetailFabEditingRow extends StatelessWidget {
final void Function() onSave;
final void Function() onCancelEdit;
ProgressDetailFabEditingRow({
Key key,
@required this.onSave,
@required this.onCancelEdit,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: FloatingActionButton(
heroTag: "saveEditedTimeProgressBTN",
child: Icon(Icons.save),
backgroundColor: Colors.green,
onPressed: this.onSave,
),
),
Expanded(
child: FloatingActionButton(
heroTag: "cancelEditTimeProgressBTN",
child: Icon(Icons.cancel),
backgroundColor: Colors.red,
onPressed: this.onCancelEdit,
))
],
);
}
}

View File

@ -1,33 +0,0 @@
import 'package:flutter/material.dart';
class ProgressDetailFabRow extends StatelessWidget {
final void Function() onEdit;
final void Function() onDelete;
ProgressDetailFabRow(
{Key key, @required this.onEdit, @required this.onDelete})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: FloatingActionButton(
heroTag: "editTimeProgressBTN",
child: Icon(Icons.edit),
onPressed: onEdit,
),
),
Expanded(
child: FloatingActionButton(
heroTag: "deleteTimeProgressBTN",
child: Icon(Icons.delete),
backgroundColor: Colors.red,
onPressed: onDelete,
),
)
],
);
}
}

View File

@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
class ProgressDetailLinearPercent extends StatelessWidget {
final TimeProgress timeProgress;
ProgressDetailLinearPercent({Key key, @required this.timeProgress})
: super(key: key);
@override
Widget build(BuildContext context) {
return LinearPercentIndicator(
padding: EdgeInsets.symmetric(horizontal: 15),
percent: this.timeProgress.percentDone(),
leading: Text("${this.timeProgress.daysBehind()} Days"),
center: Text("${(this.timeProgress.percentDone() * 100).floor()} %"),
trailing: Text("${this.timeProgress.daysLeft()} Days"),
progressColor: Colors.green,
backgroundColor: Colors.red,
lineHeight: 25,
);
}
}

View File

@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
class ProgressDetailSelectDateButton extends StatelessWidget {
final String leadingString;
final DateTime selectedDate;
final void Function(DateTime) onDateSelected;
ProgressDetailSelectDateButton({
Key key,
@required this.leadingString,
@required this.selectedDate,
@required this.onDateSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlatButton(
color: Colors.blue,
child: Text(
"$leadingString ${selectedDate.toLocal().toString().split(" ")[0]}"),
onPressed: () async {
DateTime picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(selectedDate.year - 5),
lastDate: DateTime(selectedDate.year + 5),
);
onDateSelected(picked);
},
);
}
}

View File

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/buttons/date_picker_btn.dart';
class ProgressEditorWidget extends StatefulWidget {
final TimeProgress timeProgress;
final Function(TimeProgress, bool) onTimeProgressChanged;
ProgressEditorWidget({
@required this.timeProgress,
@required this.onTimeProgressChanged,
});
@override
State<StatefulWidget> createState() {
return _ProgressEditorWidgetState();
}
}
class _ProgressEditorWidgetState extends State<ProgressEditorWidget> {
final _nameTextController = TextEditingController();
bool _validName = true, _validDate = true;
void _onNameChanged() {
TimeProgress newProgress =
widget.timeProgress.copyWith(name: _nameTextController.text);
widget.onTimeProgressChanged(
newProgress, TimeProgress.isValid(newProgress));
setState(() {
_validName = TimeProgress.isNameValid(newProgress.name);
});
}
void _onStartDateChanged(DateTime newStartDate) {
TimeProgress newProgress =
widget.timeProgress.copyWith(startTime: newStartDate);
widget.onTimeProgressChanged(
newProgress, TimeProgress.isValid(newProgress));
setState(() {
_validDate =
TimeProgress.areTimesValid(newStartDate, newProgress.endTime);
});
}
void _onEndDateChanged(DateTime newEndDate) {
TimeProgress newProgress =
widget.timeProgress.copyWith(endTime: newEndDate);
widget.onTimeProgressChanged(
newProgress, TimeProgress.isValid(newProgress));
setState(() {
_validDate =
TimeProgress.areTimesValid(newProgress.startTime, newEndDate);
});
}
@override
void initState() {
_nameTextController.text = widget.timeProgress.name;
_nameTextController.addListener(_onNameChanged);
super.initState();
}
@override
Widget build(BuildContext context) {
List<Widget> columnChildren = [
Expanded(
child: TextField(
controller: _nameTextController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Progress Name",
errorText: _validName
? null
: "The Name need to have at least 3 and at max 20 symbols.",
),
),
),
Expanded(
child: Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 5),
child: DatePickerBtn(
leadingString: "Start Date:",
pickedDate: widget.timeProgress.startTime,
onDatePicked: _onStartDateChanged,
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 5),
child: DatePickerBtn(
leadingString: "End Date:",
pickedDate: widget.timeProgress.endTime,
onDatePicked: _onEndDateChanged,
),
),
),
],
),
)
];
if (!_validDate)
columnChildren.add(
Expanded(
child: Center(
child: Text(
"Invalid Dates. The Start Date has to be before the End Date",
style: TextStyle(color: Colors.red),
),
),
),
);
return Container(
child: Column(
children: columnChildren,
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/screens/progress_detail_screen.dart';
class ProgressListTileStrings {
static String percentString(TimeProgress tp) =>
"${(tp.percentDone() * 100).floorToDouble()} %";
static String startsInDaysString(TimeProgress tp) =>
"Starts in ${tp.daysTillStart()} Days.";
static String endedDaysAgoString(TimeProgress tp) =>
"Ended ${tp.daysSinceEnd()} Days ago.";
}
class ProgressListItem extends StatelessWidget {
final TimeProgress timeProgress;
final Color doneColor, leftColor;
ProgressListItem({
@required this.timeProgress,
@required this.doneColor,
@required this.leftColor,
});
Widget _renderSubtitle(BuildContext context) {
if (!timeProgress.hasStarted())
return PlatformText(ProgressListTileStrings.startsInDaysString(timeProgress));
if (timeProgress.hasEnded())
return PlatformText(ProgressListTileStrings.endedDaysAgoString(timeProgress));
return LinearPercentIndicator(
center: PlatformText(ProgressListTileStrings.percentString(timeProgress)),
percent: timeProgress.percentDone(),
progressColor: doneColor,
backgroundColor: leftColor,
lineHeight: 20,
);
}
@override
Widget build(BuildContext context) {
void _onTileTap() =>
Navigator.pushNamed(context, ProgressDetailScreen.routeName,
arguments: ProgressDetailScreenArguments(timeProgress.id));
Text titleText = Text(timeProgress.name);
if (Platform.isIOS)
return CupertinoButton(
child: Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.fromLTRB(15, 15, 5, 5),
child: Column(
children: [
titleText,
_renderSubtitle(context),
],
),
),
onPressed: _onTileTap);
return ListTile(
title: titleText,
subtitle: _renderSubtitle(context),
onTap: _onTileTap,
);
}
}

View File

@ -0,0 +1,40 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_item.dart';
class ProgressListView extends StatelessWidget {
final List<TimeProgress> timeProgressList;
final Color doneColor, leftColor;
ProgressListView({
@required this.timeProgressList,
@required this.doneColor,
@required this.leftColor,
});
Widget _renderListTile(TimeProgress tp) {
ProgressListItem listTile = ProgressListItem(
timeProgress: tp, doneColor: doneColor, leftColor: leftColor);
if (Platform.isIOS) return listTile;
return Card(
child: listTile,
);
}
List<Widget> _renderListViewChildren() {
return timeProgressList
.map((e) => _renderListTile(e))
.toList(growable: false);
}
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(8),
children: _renderListViewChildren(),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
class ProgressViewWidget extends StatelessWidget {
final TimeProgress timeProgress;
final Color doneColor;
final Color leftColor;
ProgressViewWidget({
@required this.timeProgress,
@required this.doneColor,
@required this.leftColor,
});
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Expanded(
child: FittedBox(
fit: BoxFit.fitWidth,
child: Text(
timeProgress.name,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
),
Expanded(
child: CircularPercentIndicator(
radius: 100,
lineWidth: 10,
percent: timeProgress.percentDone(),
progressColor: doneColor,
backgroundColor: leftColor,
center: Text("${(timeProgress.percentDone() * 100).floor()} %"),
),
),
Expanded(
child: LinearPercentIndicator(
padding: EdgeInsets.symmetric(horizontal: 15),
percent: timeProgress.percentDone(),
leading: Text("${timeProgress.daysBehind()} Days"),
center: Text(
"${(timeProgress.percentDone() * 100).floor()} %",
style: TextStyle(color: Colors.white),
),
trailing: Text("${timeProgress.daysLeft()} Days"),
progressColor: doneColor,
backgroundColor: leftColor,
lineHeight: 25,
),
),
],
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/app_state.dart';
class SettingsStoreConnector extends StatelessWidget {
final Widget Function(BuildContext, SettingsViewModel) loadedBuilder;
SettingsStoreConnector({
@required this.loadedBuilder,
});
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, SettingsViewModel>(
onInit: loadSettingsIfUnloaded,
converter: (store) => SettingsViewModel._create(store),
builder: (context, SettingsViewModel vm) {
if (!vm.hasSettingsLoaded)
return Center(
child: CircularProgressIndicator(),
);
return loadedBuilder(context, vm);
},
);
}
}
class SettingsViewModel {
final AppSettings appSettings;
final bool hasSettingsLoaded;
final void Function(Color) updateDoneColor, updateLeftColor;
final void Function(Duration) updateDuration;
SettingsViewModel(
this.appSettings,
this.hasSettingsLoaded,
this.updateDoneColor,
this.updateLeftColor,
this.updateDuration,
);
factory SettingsViewModel._create(Store<AppState> store) {
AppSettings _appSettings = store.state.appSettings;
void _updateDoneColor(Color dC) => store.dispatch(
UpdateAppSettingsActions(_appSettings.copyWith(doneColor: dC)));
void _updateLeftColor(Color lC) => store.dispatch(
UpdateAppSettingsActions(_appSettings.copyWith(leftColor: lC)));
void _updateDuration(Duration d) => store
.dispatch(UpdateAppSettingsActions(_appSettings.copyWith(duration: d)));
return SettingsViewModel(_appSettings, store.state.hasSettingsLoaded,
_updateDoneColor, _updateLeftColor, _updateDuration);
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
class TimeProgressListStoreConnector extends StatelessWidget {
final Widget Function(BuildContext, TimeProgressListViewModel) loadedBuilder;
TimeProgressListStoreConnector({
@required this.loadedBuilder,
});
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, TimeProgressListViewModel>(
onInit: loadTimeProgressListIfUnloaded,
converter: (store) => TimeProgressListViewModel._create(store),
builder: (context, TimeProgressListViewModel vm) {
if (!vm.hasTpListLoaded)
return Center(
child: CircularProgressIndicator(),
);
return loadedBuilder(context, vm);
},
);
}
}
class TimeProgressListViewModel {
final List<TimeProgress> tpList;
final bool hasTpListLoaded;
TimeProgressListViewModel(this.tpList, this.hasTpListLoaded);
factory TimeProgressListViewModel._create(Store<AppState> store) =>
TimeProgressListViewModel(
store.state.timeProgressList, store.state.hasProgressesLoaded);
}

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:time_progress_tracker/actions/actions.dart';
import 'package:time_progress_tracker/models/app_state.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import '../../helper_functions.dart';
class TimeProgressStoreConnector extends StatelessWidget {
final String timeProgressId;
final Widget Function(BuildContext, TimeProgressViewModel) loadedBuilder;
TimeProgressStoreConnector({
@required this.timeProgressId,
@required this.loadedBuilder,
});
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, TimeProgressViewModel>(
onInit: loadTimeProgressListIfUnloaded,
converter: (store) =>
TimeProgressViewModel._create(store, timeProgressId),
builder: (context, TimeProgressViewModel vm) {
if (!vm.hasTpListLoaded)
return Center(
child: CircularProgressIndicator(),
);
if (vm.tp == null)
return Center(
child: Text("Error Invalid Time Progress"),
);
return loadedBuilder(context, vm);
},
);
}
}
class TimeProgressViewModel {
final TimeProgress tp;
final bool hasTpListLoaded;
final void Function(TimeProgress) updateTimeProgress;
final void Function() deleteTimeProgress;
TimeProgressViewModel(
this.tp,
this.hasTpListLoaded,
this.updateTimeProgress,
this.deleteTimeProgress,
);
factory TimeProgressViewModel._create(Store<AppState> store, String id) {
void _updateTimeProgress(TimeProgress tp) =>
store.dispatch(UpdateTimeProgressAction(id, tp));
void _deleteTimeProgress() => store.dispatch(DeleteTimeProgressAction(id));
return TimeProgressViewModel(
selectProgressById(store.state.timeProgressList, id),
store.state.hasProgressesLoaded,
_updateTimeProgress,
_deleteTimeProgress,
);
}
}

View File

@ -21,42 +21,42 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
version: "1.15.0"
convert:
dependency: transitive
description:
@ -84,26 +84,33 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "1.0.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
version: "6.1.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -111,6 +118,22 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
flutter_picker:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: e95d121f54faba889fbf8a850c86dd5cf4aa5c5a
url: "git://github.com/yangyxd/flutter_picker.git"
source: git
version: "1.1.5"
flutter_platform_widgets:
dependency: "direct main"
description:
name: flutter_platform_widgets
url: "https://pub.dartlang.org"
source: hosted
version: "0.80.0"
flutter_redux:
dependency: "direct main"
description:
@ -134,70 +157,63 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.18"
intl:
version: "2.1.19"
js:
dependency: transitive
description:
name: intl
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
version: "0.6.3"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
version: "0.12.10"
meta:
dependency: "direct main"
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3+2"
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
version: "1.8.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+2"
version: "2.0.0"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "2.0.0"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+3"
version: "2.0.0"
percent_indicator:
dependency: "direct main"
description:
name: percent_indicator
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.8"
version: "2.1.9+1"
petitparser:
dependency: transitive
description:
@ -211,21 +227,21 @@ packages:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "3.0.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "2.0.0"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.13"
version: "4.1.0"
redux:
dependency: "direct main"
description:
@ -239,42 +255,42 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.12+4"
version: "2.0.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+4"
version: "2.0.0"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+11"
version: "2.0.0"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "2.0.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2+7"
version: "2.0.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+3"
version: "2.0.0"
sky_engine:
dependency: transitive
description: flutter
@ -286,70 +302,70 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.3"
version: "2.0.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
version: "0.2.0"
xml:
dependency: transitive
description:
@ -365,5 +381,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.12.13+hotfix.5 <2.0.0"
dart: ">=2.12.0-259.9.beta <3.0.0"
flutter: ">=1.20.4"

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.0.3+3
version: 0.0.19+19
environment:
sdk: ">=2.7.0 <3.0.0"
@ -23,16 +23,15 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_colorpicker:
flutter_redux:
flutter_picker:
git: git://github.com/yangyxd/flutter_picker.git
flutter_platform_widgets:
meta:
package_info:
percent_indicator:
redux:
shared_preferences:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
dev_dependencies:

View File

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class MaterialTesterWidget extends StatelessWidget {
final Widget widget;
MaterialTesterWidget({
@required this.widget,
});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: widget,
),
);
}
}

View File

@ -7,24 +7,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:time_progress_tracker/models/app_settings.dart';
import 'package:time_progress_tracker/models/time_progress.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_item.dart';
import 'package:time_progress_tracker/widgets/progress_list_view/progress_list_view.dart';
import 'package:time_progress_calculator/main.dart';
import 'MaterialTesterWidget.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
final AppSettings _defaultAppSettings = AppSettings.defaults();
final int _thisYear = DateTime.now().year;
final TimeProgress _activeProgress = TimeProgress(
"TestProgress", DateTime(_thisYear - 2), DateTime(_thisYear + 2));
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
void _findStringOnce(String str) => expect(find.text(str), findsOneWidget);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
testWidgets("Progress List Tile with currently active progress works",
(WidgetTester tester) async {
await tester.pumpWidget(MaterialTesterWidget(
widget: ProgressListItem(
timeProgress: _activeProgress,
doneColor: _defaultAppSettings.doneColor,
leftColor: _defaultAppSettings.leftColor,
),
));
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
_findStringOnce(_activeProgress.name);
_findStringOnce(ProgressListTileStrings.percentString(_activeProgress));
WidgetPredicate linearPercentPredicate = (Widget widget) =>
widget is LinearPercentIndicator &&
widget.percent == _activeProgress.percentDone() &&
widget.progressColor == _defaultAppSettings.doneColor &&
widget.backgroundColor == _defaultAppSettings.leftColor;
expect(find.byWidgetPredicate(linearPercentPredicate), findsOneWidget);
});
testWidgets("Progress List Tile with future progress works",
(WidgetTester tester) async {
TimeProgress futureProgress = TimeProgress(
"Test Progress",
DateTime(_thisYear + 1),
DateTime(_thisYear + 2),
);
await tester.pumpWidget(MaterialTesterWidget(
widget: ProgressListItem(
timeProgress: futureProgress,
doneColor: _defaultAppSettings.doneColor,
leftColor: _defaultAppSettings.leftColor,
),
));
_findStringOnce(futureProgress.name);
_findStringOnce(ProgressListTileStrings.startsInDaysString(futureProgress));
});
testWidgets("Progress List Tile with past progress works",
(WidgetTester tester) async {
TimeProgress pastProgress = TimeProgress(
"Test Progress",
DateTime(_thisYear - 2),
DateTime(_thisYear - 1),
);
await tester.pumpWidget(MaterialTesterWidget(
widget: ProgressListItem(
timeProgress: pastProgress,
doneColor: _defaultAppSettings.doneColor,
leftColor: _defaultAppSettings.leftColor,
),
));
_findStringOnce(pastProgress.name);
_findStringOnce(ProgressListTileStrings.endedDaysAgoString(pastProgress));
});
WidgetPredicate getProgressListTilePredicate(
TimeProgress tp, AppSettings as) =>
(Widget widget) =>
widget is ProgressListItem &&
widget.timeProgress == tp &&
widget.doneColor == as.doneColor &&
widget.leftColor == as.leftColor;
testWidgets("Progress List View displays one tile",
(WidgetTester tester) async {
await tester.pumpWidget(MaterialTesterWidget(
widget: ProgressListView(
timeProgressList: [_activeProgress],
doneColor: _defaultAppSettings.doneColor,
leftColor: _defaultAppSettings.leftColor,
),
));
_findStringOnce(_activeProgress.name);
expect(
find.byWidgetPredicate(
getProgressListTilePredicate(_activeProgress, _defaultAppSettings)),
findsOneWidget);
});
testWidgets("Progress List View displays file tiles",
(WidgetTester tester) async {
List<TimeProgress> tpList = [];
for (int i = 0; i < 5; i++) tpList.add(_activeProgress);
await tester.pumpWidget(MaterialTesterWidget(
widget: ProgressListView(
timeProgressList: tpList,
doneColor: _defaultAppSettings.doneColor,
leftColor: _defaultAppSettings.leftColor,
),
));
expect(find.text(_activeProgress.name), findsNWidgets(5));
expect(
find.byWidgetPredicate(
getProgressListTilePredicate(_activeProgress, _defaultAppSettings)),
findsNWidgets(5));
});
}

Some files were not shown because too many files have changed in this diff Show More