├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── android.iml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ ├── google-services.json
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── mszalek
│ │ │ └── weight_tracker
│ │ │ └── MainActivity.java
│ │ └── res
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ └── mipmap-xxxhdpi
│ │ └── ic_launcher.png
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── google.png
├── scale-bathroom.png
└── user.png
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── Runner
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-60@2x-1.png
│ │ ├── Icon-60@2x.png
│ │ ├── Icon-60@3x.png
│ │ ├── Icon-76.png
│ │ ├── Icon-76@2x.png
│ │ ├── Icon-83.5@2x.png
│ │ ├── Icon-Notification.png
│ │ ├── Icon-Notification@3x.png
│ │ ├── Icon-Small-1.png
│ │ ├── Icon-Small-40.png
│ │ ├── Icon-Small-40@2x-1.png
│ │ ├── Icon-Small-40@2x.png
│ │ ├── Icon-Small-41.png
│ │ ├── Icon-Small-42.png
│ │ ├── Icon-Small.png
│ │ ├── Icon-Small@2x-1.png
│ │ ├── Icon-Small@2x.png
│ │ ├── Icon-Small@3x.png
│ │ └── iTunesArtwork@2x.png
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ └── main.m
├── lib
├── logic
│ ├── actions.dart
│ ├── constants.dart
│ ├── middleware.dart
│ ├── reducer.dart
│ └── redux_state.dart
├── main.dart
├── model
│ └── weight_entry.dart
├── screens
│ ├── history_page.dart
│ ├── main_page.dart
│ ├── profile_view.dart
│ ├── settings_screen.dart
│ ├── statistics_page.dart
│ └── weight_entry_dialog.dart
└── widgets
│ ├── progress_chart.dart
│ ├── progress_chart_dropdown.dart
│ ├── progress_chart_utils.dart
│ └── weight_list_item.dart
├── pubspec.yaml
├── test
├── unit_tests
│ ├── chart_painter_test.dart
│ ├── middleware_test.dart
│ ├── progress_chart_utils_test.dart
│ └── reducer_test.dart
└── widget_tests
│ ├── chart_dropdown_test.dart
│ ├── history_page_test.dart
│ ├── main_page_test.dart
│ ├── settings_page_test.dart
│ ├── weight_entry_dialog_test.dart
│ └── weight_list_item_test.dart
└── weight_tracker.iml
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .atom/
3 | .idea
4 | .packages
5 | .pub/
6 | build/
7 | ios/.generated/
8 | packages
9 | pubspec.lock
10 | .flutter-plugins
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os:
2 | - linux
3 | sudo: false
4 | addons:
5 | apt:
6 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18
7 | sources:
8 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version
9 | packages:
10 | - libstdc++6
11 | - fonts-droid
12 | before_script:
13 | - git clone https://github.com/flutter/flutter.git -b beta --depth 1
14 | - ./flutter/bin/flutter doctor
15 | - gem install coveralls-lcov
16 | script:
17 | - ./flutter/bin/flutter test --coverage
18 | after_success:
19 | - coveralls-lcov coverage/lcov.info
20 | cache:
21 | directories:
22 | - $HOME/.pub-cache
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeightTracker [](https://app.nevercode.io/#/project/c3ffa8f5-0afe-45b1-afeb-67478b294cba/workflow/1e254e1b-5b96-4e00-ab37-c4347e44dd1d/latestBuild?branch=master) [](https://travis-ci.org/MSzalek-Mobile/weight_tracker) [](https://coveralls.io/github/MSzalek-Mobile/weight_tracker?branch=master)
2 |
3 |
4 |
5 | Simple application for tracking weight. See Google Play for more details about this app!
6 |
7 | ## Getting started
8 |
9 | To build the app you need to create your own Firebase application according to guidlines on [Firebase codelab](https://codelabs.developers.google.com/codelabs/flutter-firebase/#4).
10 |
11 | For android you need to provide own `google-services.json` file. It is explained in [codelab](https://codelabs.developers.google.com/codelabs/flutter-firebase/#4) and [here](https://firebase.google.com/docs/android/setup?authuser=0).
12 |
13 | ## Contributing
14 |
15 | Feel free to add issues with bugs or ideas. Any pull requests are very welcome!
16 |
--------------------------------------------------------------------------------
/android.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | GeneratedPluginRegistrant.java
10 |
11 | /gradlew
12 | /gradlew.bat
13 | /gradle/wrapper/gradle-wrapper.jar
14 |
15 | key.jks
16 | key.properties
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withInputStream { stream ->
5 | localProperties.load(stream)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def keystorePropertiesFile = rootProject.file("key.properties")
15 | def keystoreProperties = new Properties()
16 | if (keystorePropertiesFile.exists()) {
17 | keystorePropertiesFile.withInputStream { stream ->
18 | keystoreProperties.load(stream)
19 | }
20 | }
21 | //keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
22 |
23 | apply plugin: 'com.android.application'
24 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
25 |
26 | android {
27 | compileSdkVersion 28
28 |
29 | lintOptions {
30 | disable 'InvalidPackage'
31 | }
32 |
33 | defaultConfig {
34 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
35 | minSdkVersion 16
36 | targetSdkVersion 28
37 | versionCode 10
38 | versionName "1.5.3"
39 |
40 | applicationId "com.mszalek.weight_tracker"
41 | }
42 |
43 | if (keystorePropertiesFile.exists()) {
44 | signingConfigs {
45 | release {
46 | keyAlias keystoreProperties['keyAlias']
47 | keyPassword keystoreProperties['keyPassword']
48 | storeFile file(keystoreProperties['storeFile'])
49 | storePassword keystoreProperties['storePassword']
50 | }
51 |
52 | }
53 | }
54 | buildTypes {
55 | release {
56 | if (keystorePropertiesFile.exists()) {
57 | signingConfig signingConfigs.release
58 |
59 | } else {
60 | signingConfig signingConfigs.debug
61 | }
62 | }
63 | }
64 | }
65 |
66 | flutter {
67 | source '../..'
68 | }
69 |
70 | dependencies {
71 | testImplementation 'junit:junit:4.12'
72 | androidTestImplementation 'com.android.support.test:runner:1.0.1'
73 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
74 | }
75 | apply plugin: 'com.google.gms.google-services'
76 |
--------------------------------------------------------------------------------
/android/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "123595545975",
4 | "firebase_url": "https://weight-tracker-e574f.firebaseio.com",
5 | "project_id": "weight-tracker-e574f",
6 | "storage_bucket": "weight-tracker-e574f.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:123595545975:android:94795a974b1f6481",
12 | "android_client_info": {
13 | "package_name": "com.mszalek.weight_tracker"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "123595545975-cv0hdhccs4o3itmi7k70o4d8i61injvi.apps.googleusercontent.com",
19 | "client_type": 1,
20 | "android_info": {
21 | "package_name": "com.mszalek.weight_tracker",
22 | "certificate_hash": "4cfcaae693f041e62c51224b17ff5920ca45db7c"
23 | }
24 | },
25 | {
26 | "client_id": "123595545975-qlq2eju9tpm89igcgj13f96dne92dd12.apps.googleusercontent.com",
27 | "client_type": 1,
28 | "android_info": {
29 | "package_name": "com.mszalek.weight_tracker",
30 | "certificate_hash": "52f32b889e107f33b006c376421992370f90510c"
31 | }
32 | },
33 | {
34 | "client_id": "123595545975-jvcpng9krrieeokh79673e448cekctnj.apps.googleusercontent.com",
35 | "client_type": 3
36 | }
37 | ],
38 | "api_key": [
39 | {
40 | "current_key": "AIzaSyALrYc3bKgEoGPX2Ez3jjgejLRqaNXSysY"
41 | }
42 | ],
43 | "services": {
44 | "analytics_service": {
45 | "status": 1
46 | },
47 | "appinvite_service": {
48 | "status": 2,
49 | "other_platform_oauth_client": [
50 | {
51 | "client_id": "123595545975-jvcpng9krrieeokh79673e448cekctnj.apps.googleusercontent.com",
52 | "client_type": 3
53 | }
54 | ]
55 | },
56 | "ads_service": {
57 | "status": 2
58 | }
59 | }
60 | }
61 | ],
62 | "configuration_version": "1"
63 | }
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
19 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/mszalek/weight_tracker/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.mszalek.weight_tracker;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import com.google.android.gms.actions.NoteIntents;
6 | import io.flutter.app.FlutterActivity;
7 | import io.flutter.plugin.common.MethodCall;
8 | import io.flutter.plugin.common.MethodChannel;
9 | import io.flutter.plugins.GeneratedPluginRegistrant;
10 |
11 | public class MainActivity extends FlutterActivity {
12 | String savedNote;
13 |
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 | GeneratedPluginRegistrant.registerWith(this);
18 | Intent intent = getIntent();
19 | String action = intent.getAction();
20 | String type = intent.getType();
21 |
22 | if (NoteIntents.ACTION_CREATE_NOTE.equals(action) && type != null) {
23 | if ("text/plain".equals(type)) {
24 | handleSendText(intent);
25 | }
26 | }
27 |
28 | new MethodChannel(getFlutterView(), "app.channel.shared.data")
29 | .setMethodCallHandler(new MethodChannel.MethodCallHandler() {
30 | @Override
31 | public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
32 | if (methodCall.method.contentEquals("getSavedNote")) {
33 | result.success(savedNote);
34 | savedNote = null;
35 | }
36 | }
37 | });
38 | }
39 |
40 |
41 | void handleSendText(Intent intent) {
42 | savedNote = intent.getStringExtra(Intent.EXTRA_TEXT);
43 | }
44 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | jcenter()
5 | }
6 |
7 | dependencies {
8 | // Example existing classpath
9 | classpath 'com.android.tools.build:gradle:3.2.1'
10 | // Add the google services classpath
11 | classpath 'com.google.gms:google-services:4.2.0'
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | }
21 |
22 | rootProject.buildDir = '../build'
23 | subprojects {
24 | project.buildDir = "${rootProject.buildDir}/${project.name}"
25 | project.evaluationDependsOn(':app')
26 | }
27 |
28 | task clean(type: Delete) {
29 | delete rootProject.buildDir
30 | }
31 |
32 | task wrapper(type: Wrapper) {
33 | gradleVersion = '2.14.1'
34 | }
35 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withInputStream { stream -> plugins.load(stream) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/assets/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/assets/google.png
--------------------------------------------------------------------------------
/assets/scale-bathroom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/assets/scale-bathroom.png
--------------------------------------------------------------------------------
/assets/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/assets/user.png
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | *.pbxuser
16 | *.mode1v3
17 | *.mode2v3
18 | *.perspectivev3
19 |
20 | !default.pbxuser
21 | !default.mode1v3
22 | !default.mode2v3
23 | !default.perspectivev3
24 |
25 | xcuserdata
26 |
27 | *.moved-aside
28 |
29 | *.pyc
30 | *sync/
31 | Icon?
32 | .tags*
33 |
34 | /Flutter/app.flx
35 | /Flutter/app.zip
36 | /Flutter/App.framework
37 | /Flutter/Flutter.framework
38 | /Flutter/Generated.xcconfig
39 | /ServiceDefinitions.json
40 |
41 | Pods/
42 |
43 | Runner/GoogleService-Info.plist
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | UIRequiredDeviceCapabilities
24 |
25 | arm64
26 |
27 | MinimumOSVersion
28 | 8.0
29 |
30 |
31 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | if ENV['FLUTTER_FRAMEWORK_DIR'] == nil
5 | abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework')
6 | end
7 |
8 | target 'Runner' do
9 | use_frameworks!
10 |
11 | # Pods for Runner
12 |
13 | # Flutter Pods
14 | pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR']
15 |
16 | if File.exists? '../.flutter-plugins'
17 | flutter_root = File.expand_path('..')
18 | File.foreach('../.flutter-plugins') { |line|
19 | plugin = line.split(pattern='=')
20 | if plugin.length == 2
21 | name = plugin[0].strip()
22 | path = plugin[1].strip()
23 | resolved_path = File.expand_path("#{path}/ios", flutter_root)
24 | pod name, :path => resolved_path
25 | else
26 | puts "Invalid plugin specification: #{line}"
27 | end
28 | }
29 | end
30 | end
31 |
32 | post_install do |installer|
33 | installer.pods_project.targets.each do |target|
34 | target.build_configurations.each do |config|
35 | config.build_settings['ENABLE_BITCODE'] = 'NO'
36 | end
37 | end
38 | end
39 |
40 | pod 'Firebase/Core'
41 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 10A14849218CAC850092B9F5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 10A14848218CAC850092B9F5 /* GoogleService-Info.plist */; };
11 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
12 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
13 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
14 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
15 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
16 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
17 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
18 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
19 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; };
20 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
21 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
22 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
23 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
24 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
25 | BD557365A178B518B06E24A2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D24E04E03F831953FB76E34 /* Pods_Runner.framework */; };
26 | /* End PBXBuildFile section */
27 |
28 | /* Begin PBXCopyFilesBuildPhase section */
29 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
30 | isa = PBXCopyFilesBuildPhase;
31 | buildActionMask = 2147483647;
32 | dstPath = "";
33 | dstSubfolderSpec = 10;
34 | files = (
35 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
36 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
37 | );
38 | name = "Embed Frameworks";
39 | runOnlyForDeploymentPostprocessing = 0;
40 | };
41 | /* End PBXCopyFilesBuildPhase section */
42 |
43 | /* Begin PBXFileReference section */
44 | 10A14848218CAC850092B9F5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
45 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
46 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
47 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
48 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
49 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
50 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
51 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
52 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; tabWidth = 2; };
53 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
54 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
55 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
56 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
57 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
58 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
59 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
60 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
61 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
62 | 9D24E04E03F831953FB76E34 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
63 | /* End PBXFileReference section */
64 |
65 | /* Begin PBXFrameworksBuildPhase section */
66 | 97C146EB1CF9000F007C117D /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
71 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
72 | BD557365A178B518B06E24A2 /* Pods_Runner.framework in Frameworks */,
73 | );
74 | runOnlyForDeploymentPostprocessing = 0;
75 | };
76 | /* End PBXFrameworksBuildPhase section */
77 |
78 | /* Begin PBXGroup section */
79 | 1A2FD378E985B3C92B55F41C /* Pods */ = {
80 | isa = PBXGroup;
81 | children = (
82 | );
83 | name = Pods;
84 | sourceTree = "";
85 | };
86 | 46EA35BEFF623C38CC22B7D9 /* Frameworks */ = {
87 | isa = PBXGroup;
88 | children = (
89 | 9D24E04E03F831953FB76E34 /* Pods_Runner.framework */,
90 | );
91 | name = Frameworks;
92 | sourceTree = "";
93 | };
94 | 9740EEB11CF90186004384FC /* Flutter */ = {
95 | isa = PBXGroup;
96 | children = (
97 | 3B80C3931E831B6300D905FE /* App.framework */,
98 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
99 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
100 | 9740EEBA1CF902C7004384FC /* Flutter.framework */,
101 | 9740EEB21CF90195004384FC /* Debug.xcconfig */,
102 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
103 | 9740EEB31CF90195004384FC /* Generated.xcconfig */,
104 | );
105 | name = Flutter;
106 | sourceTree = "";
107 | };
108 | 97C146E51CF9000F007C117D = {
109 | isa = PBXGroup;
110 | children = (
111 | 9740EEB11CF90186004384FC /* Flutter */,
112 | 97C146F01CF9000F007C117D /* Runner */,
113 | 97C146EF1CF9000F007C117D /* Products */,
114 | 1A2FD378E985B3C92B55F41C /* Pods */,
115 | 46EA35BEFF623C38CC22B7D9 /* Frameworks */,
116 | );
117 | sourceTree = "";
118 | };
119 | 97C146EF1CF9000F007C117D /* Products */ = {
120 | isa = PBXGroup;
121 | children = (
122 | 97C146EE1CF9000F007C117D /* Runner.app */,
123 | );
124 | name = Products;
125 | sourceTree = "";
126 | };
127 | 97C146F01CF9000F007C117D /* Runner */ = {
128 | isa = PBXGroup;
129 | children = (
130 | 10A14848218CAC850092B9F5 /* GoogleService-Info.plist */,
131 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
132 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
133 | 97C146FA1CF9000F007C117D /* Main.storyboard */,
134 | 97C146FD1CF9000F007C117D /* Assets.xcassets */,
135 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
136 | 97C147021CF9000F007C117D /* Info.plist */,
137 | 97C146F11CF9000F007C117D /* Supporting Files */,
138 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
139 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
140 | );
141 | path = Runner;
142 | sourceTree = "";
143 | };
144 | 97C146F11CF9000F007C117D /* Supporting Files */ = {
145 | isa = PBXGroup;
146 | children = (
147 | 97C146F21CF9000F007C117D /* main.m */,
148 | );
149 | name = "Supporting Files";
150 | sourceTree = "";
151 | };
152 | /* End PBXGroup section */
153 |
154 | /* Begin PBXNativeTarget section */
155 | 97C146ED1CF9000F007C117D /* Runner */ = {
156 | isa = PBXNativeTarget;
157 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
158 | buildPhases = (
159 | 7CA0A0BB3DBE573251D6E4FA /* [CP] Check Pods Manifest.lock */,
160 | 9740EEB61CF901F6004384FC /* Run Script */,
161 | 97C146EA1CF9000F007C117D /* Sources */,
162 | 97C146EB1CF9000F007C117D /* Frameworks */,
163 | 97C146EC1CF9000F007C117D /* Resources */,
164 | 9705A1C41CF9048500538489 /* Embed Frameworks */,
165 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
166 | 7BE0A878EB606EC8E0233670 /* [CP] Embed Pods Frameworks */,
167 | BF931F3CCD1020D60524CFAF /* [CP] Copy Pods Resources */,
168 | );
169 | buildRules = (
170 | );
171 | dependencies = (
172 | );
173 | name = Runner;
174 | productName = Runner;
175 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
176 | productType = "com.apple.product-type.application";
177 | };
178 | /* End PBXNativeTarget section */
179 |
180 | /* Begin PBXProject section */
181 | 97C146E61CF9000F007C117D /* Project object */ = {
182 | isa = PBXProject;
183 | attributes = {
184 | LastUpgradeCheck = 0830;
185 | ORGANIZATIONNAME = "The Chromium Authors";
186 | TargetAttributes = {
187 | 97C146ED1CF9000F007C117D = {
188 | CreatedOnToolsVersion = 7.3.1;
189 | DevelopmentTeam = 3RRHFRV4Q4;
190 | };
191 | };
192 | };
193 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
194 | compatibilityVersion = "Xcode 3.2";
195 | developmentRegion = English;
196 | hasScannedForEncodings = 0;
197 | knownRegions = (
198 | en,
199 | Base,
200 | );
201 | mainGroup = 97C146E51CF9000F007C117D;
202 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
203 | projectDirPath = "";
204 | projectRoot = "";
205 | targets = (
206 | 97C146ED1CF9000F007C117D /* Runner */,
207 | );
208 | };
209 | /* End PBXProject section */
210 |
211 | /* Begin PBXResourcesBuildPhase section */
212 | 97C146EC1CF9000F007C117D /* Resources */ = {
213 | isa = PBXResourcesBuildPhase;
214 | buildActionMask = 2147483647;
215 | files = (
216 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
217 | 10A14849218CAC850092B9F5 /* GoogleService-Info.plist in Resources */,
218 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */,
219 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
220 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
221 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
222 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
223 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
224 | );
225 | runOnlyForDeploymentPostprocessing = 0;
226 | };
227 | /* End PBXResourcesBuildPhase section */
228 |
229 | /* Begin PBXShellScriptBuildPhase section */
230 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
231 | isa = PBXShellScriptBuildPhase;
232 | buildActionMask = 2147483647;
233 | files = (
234 | );
235 | inputPaths = (
236 | );
237 | name = "Thin Binary";
238 | outputPaths = (
239 | );
240 | runOnlyForDeploymentPostprocessing = 0;
241 | shellPath = /bin/sh;
242 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
243 | };
244 | 7BE0A878EB606EC8E0233670 /* [CP] Embed Pods Frameworks */ = {
245 | isa = PBXShellScriptBuildPhase;
246 | buildActionMask = 2147483647;
247 | files = (
248 | );
249 | inputPaths = (
250 | "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
251 | "${PODS_ROOT}/../../../../../flutter/bin/cache/artifacts/engine/ios/Flutter.framework",
252 | "${BUILT_PRODUCTS_DIR}/GTMOAuth2/GTMOAuth2.framework",
253 | "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
254 | "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
255 | "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
256 | "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
257 | "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
258 | "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
259 | );
260 | name = "[CP] Embed Pods Frameworks";
261 | outputPaths = (
262 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
263 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMOAuth2.framework",
264 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
265 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
266 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
267 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
268 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
269 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
270 | );
271 | runOnlyForDeploymentPostprocessing = 0;
272 | shellPath = /bin/sh;
273 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
274 | showEnvVarsInLog = 0;
275 | };
276 | 7CA0A0BB3DBE573251D6E4FA /* [CP] Check Pods Manifest.lock */ = {
277 | isa = PBXShellScriptBuildPhase;
278 | buildActionMask = 2147483647;
279 | files = (
280 | );
281 | inputPaths = (
282 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
283 | "${PODS_ROOT}/Manifest.lock",
284 | );
285 | name = "[CP] Check Pods Manifest.lock";
286 | outputPaths = (
287 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
288 | );
289 | runOnlyForDeploymentPostprocessing = 0;
290 | shellPath = /bin/sh;
291 | 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";
292 | showEnvVarsInLog = 0;
293 | };
294 | 9740EEB61CF901F6004384FC /* Run Script */ = {
295 | isa = PBXShellScriptBuildPhase;
296 | buildActionMask = 2147483647;
297 | files = (
298 | );
299 | inputPaths = (
300 | );
301 | name = "Run Script";
302 | outputPaths = (
303 | );
304 | runOnlyForDeploymentPostprocessing = 0;
305 | shellPath = /bin/sh;
306 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
307 | };
308 | BF931F3CCD1020D60524CFAF /* [CP] Copy Pods Resources */ = {
309 | isa = PBXShellScriptBuildPhase;
310 | buildActionMask = 2147483647;
311 | files = (
312 | );
313 | inputPaths = (
314 | "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
315 | "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
316 | );
317 | name = "[CP] Copy Pods Resources";
318 | outputPaths = (
319 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
320 | );
321 | runOnlyForDeploymentPostprocessing = 0;
322 | shellPath = /bin/sh;
323 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
324 | showEnvVarsInLog = 0;
325 | };
326 | /* End PBXShellScriptBuildPhase section */
327 |
328 | /* Begin PBXSourcesBuildPhase section */
329 | 97C146EA1CF9000F007C117D /* Sources */ = {
330 | isa = PBXSourcesBuildPhase;
331 | buildActionMask = 2147483647;
332 | files = (
333 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
334 | 97C146F31CF9000F007C117D /* main.m in Sources */,
335 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
336 | );
337 | runOnlyForDeploymentPostprocessing = 0;
338 | };
339 | /* End PBXSourcesBuildPhase section */
340 |
341 | /* Begin PBXVariantGroup section */
342 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
343 | isa = PBXVariantGroup;
344 | children = (
345 | 97C146FB1CF9000F007C117D /* Base */,
346 | );
347 | name = Main.storyboard;
348 | sourceTree = "";
349 | };
350 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
351 | isa = PBXVariantGroup;
352 | children = (
353 | 97C147001CF9000F007C117D /* Base */,
354 | );
355 | name = LaunchScreen.storyboard;
356 | sourceTree = "";
357 | };
358 | /* End PBXVariantGroup section */
359 |
360 | /* Begin XCBuildConfiguration section */
361 | 97C147031CF9000F007C117D /* Debug */ = {
362 | isa = XCBuildConfiguration;
363 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
364 | buildSettings = {
365 | ALWAYS_SEARCH_USER_PATHS = NO;
366 | CLANG_ANALYZER_NONNULL = YES;
367 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
368 | CLANG_CXX_LIBRARY = "libc++";
369 | CLANG_ENABLE_MODULES = YES;
370 | CLANG_ENABLE_OBJC_ARC = YES;
371 | CLANG_WARN_BOOL_CONVERSION = YES;
372 | CLANG_WARN_CONSTANT_CONVERSION = YES;
373 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
374 | CLANG_WARN_EMPTY_BODY = YES;
375 | CLANG_WARN_ENUM_CONVERSION = YES;
376 | CLANG_WARN_INFINITE_RECURSION = YES;
377 | CLANG_WARN_INT_CONVERSION = YES;
378 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
379 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
380 | CLANG_WARN_UNREACHABLE_CODE = YES;
381 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
382 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
383 | COPY_PHASE_STRIP = NO;
384 | DEBUG_INFORMATION_FORMAT = dwarf;
385 | ENABLE_STRICT_OBJC_MSGSEND = YES;
386 | ENABLE_TESTABILITY = YES;
387 | GCC_C_LANGUAGE_STANDARD = gnu99;
388 | GCC_DYNAMIC_NO_PIC = NO;
389 | GCC_NO_COMMON_BLOCKS = YES;
390 | GCC_OPTIMIZATION_LEVEL = 0;
391 | GCC_PREPROCESSOR_DEFINITIONS = (
392 | "DEBUG=1",
393 | "$(inherited)",
394 | );
395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
396 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
397 | GCC_WARN_UNDECLARED_SELECTOR = YES;
398 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
399 | GCC_WARN_UNUSED_FUNCTION = YES;
400 | GCC_WARN_UNUSED_VARIABLE = YES;
401 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
402 | MTL_ENABLE_DEBUG_INFO = YES;
403 | ONLY_ACTIVE_ARCH = YES;
404 | SDKROOT = iphoneos;
405 | TARGETED_DEVICE_FAMILY = "1,2";
406 | };
407 | name = Debug;
408 | };
409 | 97C147041CF9000F007C117D /* Release */ = {
410 | isa = XCBuildConfiguration;
411 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
412 | buildSettings = {
413 | ALWAYS_SEARCH_USER_PATHS = NO;
414 | CLANG_ANALYZER_NONNULL = YES;
415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
416 | CLANG_CXX_LIBRARY = "libc++";
417 | CLANG_ENABLE_MODULES = YES;
418 | CLANG_ENABLE_OBJC_ARC = YES;
419 | CLANG_WARN_BOOL_CONVERSION = YES;
420 | CLANG_WARN_CONSTANT_CONVERSION = YES;
421 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
422 | CLANG_WARN_EMPTY_BODY = YES;
423 | CLANG_WARN_ENUM_CONVERSION = YES;
424 | CLANG_WARN_INFINITE_RECURSION = YES;
425 | CLANG_WARN_INT_CONVERSION = YES;
426 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
427 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
428 | CLANG_WARN_UNREACHABLE_CODE = YES;
429 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
430 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
431 | COPY_PHASE_STRIP = NO;
432 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
433 | ENABLE_NS_ASSERTIONS = NO;
434 | ENABLE_STRICT_OBJC_MSGSEND = YES;
435 | GCC_C_LANGUAGE_STANDARD = gnu99;
436 | GCC_NO_COMMON_BLOCKS = YES;
437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
438 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
439 | GCC_WARN_UNDECLARED_SELECTOR = YES;
440 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
441 | GCC_WARN_UNUSED_FUNCTION = YES;
442 | GCC_WARN_UNUSED_VARIABLE = YES;
443 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
444 | MTL_ENABLE_DEBUG_INFO = NO;
445 | SDKROOT = iphoneos;
446 | TARGETED_DEVICE_FAMILY = "1,2";
447 | VALIDATE_PRODUCT = YES;
448 | };
449 | name = Release;
450 | };
451 | 97C147061CF9000F007C117D /* Debug */ = {
452 | isa = XCBuildConfiguration;
453 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
454 | buildSettings = {
455 | ARCHS = arm64;
456 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
457 | DEVELOPMENT_TEAM = 3RRHFRV4Q4;
458 | ENABLE_BITCODE = NO;
459 | FRAMEWORK_SEARCH_PATHS = (
460 | "$(inherited)",
461 | "$(PROJECT_DIR)/Flutter",
462 | );
463 | INFOPLIST_FILE = Runner/Info.plist;
464 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
465 | LIBRARY_SEARCH_PATHS = (
466 | "$(inherited)",
467 | "$(PROJECT_DIR)/Flutter",
468 | );
469 | PRODUCT_BUNDLE_IDENTIFIER = com.mszalek.weighttracker;
470 | PRODUCT_NAME = "$(TARGET_NAME)";
471 | };
472 | name = Debug;
473 | };
474 | 97C147071CF9000F007C117D /* Release */ = {
475 | isa = XCBuildConfiguration;
476 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
477 | buildSettings = {
478 | ARCHS = arm64;
479 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
480 | DEVELOPMENT_TEAM = 3RRHFRV4Q4;
481 | ENABLE_BITCODE = NO;
482 | FRAMEWORK_SEARCH_PATHS = (
483 | "$(inherited)",
484 | "$(PROJECT_DIR)/Flutter",
485 | );
486 | INFOPLIST_FILE = Runner/Info.plist;
487 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
488 | LIBRARY_SEARCH_PATHS = (
489 | "$(inherited)",
490 | "$(PROJECT_DIR)/Flutter",
491 | );
492 | PRODUCT_BUNDLE_IDENTIFIER = com.mszalek.weighttracker;
493 | PRODUCT_NAME = "$(TARGET_NAME)";
494 | };
495 | name = Release;
496 | };
497 | /* End XCBuildConfiguration section */
498 |
499 | /* Begin XCConfigurationList section */
500 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
501 | isa = XCConfigurationList;
502 | buildConfigurations = (
503 | 97C147031CF9000F007C117D /* Debug */,
504 | 97C147041CF9000F007C117D /* Release */,
505 | );
506 | defaultConfigurationIsVisible = 0;
507 | defaultConfigurationName = Release;
508 | };
509 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
510 | isa = XCConfigurationList;
511 | buildConfigurations = (
512 | 97C147061CF9000F007C117D /* Debug */,
513 | 97C147071CF9000F007C117D /* Release */,
514 | );
515 | defaultConfigurationIsVisible = 0;
516 | defaultConfigurationName = Release;
517 | };
518 | /* End XCConfigurationList section */
519 | };
520 | rootObject = 97C146E61CF9000F007C117D /* Project object */;
521 | }
522 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildSystemType
6 | Original
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : FlutterAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.m:
--------------------------------------------------------------------------------
1 | #include "AppDelegate.h"
2 | #include "GeneratedPluginRegistrant.h"
3 | @import Firebase;
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
8 | [GeneratedPluginRegistrant registerWithRegistry:self];
9 | // Override point for customization after application launch.
10 | // [FIRApp configure];
11 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
12 | }
13 |
14 | @end
15 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-Small-40.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-Notification@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-Small.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-Small@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-Small@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-Small-40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-60@2x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-60@2x-1.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-Notification.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-Small-41.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-Small-1.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-Small@2x-1.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-Small-42.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-Small-40@2x-1.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-76.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "iTunesArtwork@2x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-41.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-42.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yummo8/weight-tracker-flutter/7106e06100aa6950250e826dce0b9babc6b85773/ios/Runner/Assets.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Weight Tracker
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | weight_tracker
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UIRequiredDeviceCapabilities
32 |
33 | arm64
34 |
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationLandscapeLeft
39 | UIInterfaceOrientationLandscapeRight
40 |
41 | UISupportedInterfaceOrientations~ipad
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationPortraitUpsideDown
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UIViewControllerBasedStatusBarAppearance
49 |
50 | CFBundleURLTypes
51 |
52 |
53 | CFBundleTypeRole
54 | Editor
55 | CFBundleURLSchemes
56 |
57 | com.googleusercontent.apps.123595545975-95c8frhlr8jrhkkdo8n7oikrc4d9qso1
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/ios/Runner/main.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char * argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil,
8 | NSStringFromClass([AppDelegate class]));
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/logic/actions.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:firebase_database/firebase_database.dart';
3 | import 'package:weight_tracker/model/weight_entry.dart';
4 |
5 | class UserLoadedAction {
6 | final FirebaseUser firebaseUser;
7 | final List cachedEntries;
8 |
9 | UserLoadedAction(this.firebaseUser, {this.cachedEntries = const []});
10 | }
11 |
12 | class AddDatabaseReferenceAction {
13 | final DatabaseReference databaseReference;
14 | final List cachedEntries;
15 |
16 | AddDatabaseReferenceAction(this.databaseReference,
17 | {this.cachedEntries = const []});
18 | }
19 |
20 | class GetSavedWeightNote {}
21 |
22 | class AddWeightFromNotes {
23 | final double weight;
24 |
25 | AddWeightFromNotes(this.weight);
26 | }
27 |
28 | class ConsumeWeightFromNotes {}
29 |
30 | class AddEntryAction {
31 | final WeightEntry weightEntry;
32 |
33 | AddEntryAction(this.weightEntry);
34 | }
35 |
36 | class EditEntryAction {
37 | final WeightEntry weightEntry;
38 |
39 | EditEntryAction(this.weightEntry);
40 | }
41 |
42 | class RemoveEntryAction {
43 | final WeightEntry weightEntry;
44 |
45 | RemoveEntryAction(this.weightEntry);
46 | }
47 |
48 | class OnAddedAction {
49 | final Event event;
50 |
51 | OnAddedAction(this.event);
52 | }
53 |
54 | class OnChangedAction {
55 | final Event event;
56 |
57 | OnChangedAction(this.event);
58 | }
59 |
60 | class OnRemovedAction {
61 | final Event event;
62 |
63 | OnRemovedAction(this.event);
64 | }
65 |
66 | class AcceptEntryAddedAction {}
67 |
68 | class AcceptEntryRemovalAction {}
69 |
70 | class UndoRemovalAction {}
71 |
72 | class InitAction {}
73 |
74 | class SetUnitAction {
75 | final String unit;
76 |
77 | SetUnitAction(this.unit);
78 | }
79 |
80 | class OnUnitChangedAction {
81 | final String unit;
82 |
83 | OnUnitChangedAction(this.unit);
84 | }
85 |
86 | class UpdateActiveWeightEntry {
87 | final WeightEntry weightEntry;
88 |
89 | UpdateActiveWeightEntry(this.weightEntry);
90 | }
91 |
92 | class OpenAddEntryDialog {}
93 |
94 | class OpenEditEntryDialog {
95 | final WeightEntry weightEntry;
96 |
97 | OpenEditEntryDialog(this.weightEntry);
98 | }
99 |
100 | class ChangeProgressChartStartDate {
101 | final DateTime dateTime;
102 |
103 | ChangeProgressChartStartDate(this.dateTime);
104 | }
105 |
106 | class LoginWithGoogle {
107 | final List cachedEntries;
108 |
109 | LoginWithGoogle({this.cachedEntries = const []});
110 | }
111 |
112 | class LogoutAction {
113 | LogoutAction();
114 | }
115 |
--------------------------------------------------------------------------------
/lib/logic/constants.dart:
--------------------------------------------------------------------------------
1 | const double KG_LBS_RATIO = 2.2;
2 | const int MAX_KG_VALUE = 200;
3 | const int MIN_KG_VALUE = 5;
--------------------------------------------------------------------------------
/lib/logic/middleware.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:firebase_auth/firebase_auth.dart';
4 | import 'package:firebase_database/firebase_database.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:google_sign_in/google_sign_in.dart';
7 | import 'package:redux/redux.dart';
8 | import 'package:shared_preferences/shared_preferences.dart';
9 | import 'package:weight_tracker/logic/actions.dart';
10 | import 'package:weight_tracker/logic/constants.dart';
11 | import 'package:weight_tracker/logic/redux_state.dart';
12 | import 'package:weight_tracker/model/weight_entry.dart';
13 |
14 | final GoogleSignIn _googleSignIn = new GoogleSignIn();
15 |
16 | middleware(Store store, action, NextDispatcher next) {
17 | print(action.runtimeType);
18 | if (action is InitAction) {
19 | _handleInitAction(store);
20 | } else if (action is AddEntryAction) {
21 | _handleAddEntryAction(store, action);
22 | } else if (action is EditEntryAction) {
23 | _handleEditEntryAction(store, action);
24 | } else if (action is RemoveEntryAction) {
25 | _handleRemoveEntryAction(store, action);
26 | } else if (action is UndoRemovalAction) {
27 | _handleUndoRemovalAction(store);
28 | } else if (action is SetUnitAction) {
29 | _handleSetUnitAction(action, store);
30 | } else if (action is GetSavedWeightNote) {
31 | _handleGetSavedWeightNote(store);
32 | } else if (action is AddWeightFromNotes) {
33 | _handleAddWeightFromNotes(store, action);
34 | } else if (action is LoginWithGoogle) {
35 | _handleLoginWithGoogle(store, action);
36 | } else if (action is LogoutAction) {
37 | _handleLogoutAction(store, action);
38 | }
39 | next(action);
40 | if (action is UserLoadedAction) {
41 | _handleUserLoadedAction(store, action);
42 | } else if (action is AddDatabaseReferenceAction) {
43 | _handleAddedDatabaseReference(store, action);
44 | }
45 | }
46 |
47 | _handleLogoutAction(Store store, LogoutAction action) {
48 | _googleSignIn.signOut();
49 | FirebaseAuth.instance.signOut().then((_) => FirebaseAuth.instance
50 | .signInAnonymously()
51 | .then((user) => store.dispatch(UserLoadedAction(user))));
52 | }
53 |
54 | _handleLoginWithGoogle(Store store, LoginWithGoogle action) async {
55 | GoogleSignInAccount googleUser = await _getGoogleUser();
56 | GoogleSignInAuthentication credentials = await googleUser.authentication;
57 |
58 | bool hasLinkingFailed = false;
59 | try {
60 | await FirebaseAuth.instance.linkWithGoogleCredential(
61 | idToken: credentials.idToken,
62 | accessToken: credentials.accessToken,
63 | );
64 | } catch (e) {
65 | await FirebaseAuth.instance.signInWithGoogle(
66 | idToken: credentials.idToken,
67 | accessToken: credentials.accessToken,
68 | );
69 | hasLinkingFailed = true;
70 | }
71 |
72 | FirebaseUser user = await FirebaseAuth.instance.currentUser();
73 | await user.updateProfile(new UserUpdateInfo()
74 | ..photoUrl = googleUser.photoUrl
75 | ..displayName = googleUser.displayName);
76 | user.reload();
77 |
78 | store.dispatch(new UserLoadedAction(
79 | user,
80 | cachedEntries: hasLinkingFailed ? action.cachedEntries : [],
81 | ));
82 | }
83 |
84 | Future _getGoogleUser() async {
85 | GoogleSignInAccount googleUser = _googleSignIn.currentUser;
86 | if (googleUser == null) {
87 | googleUser = await _googleSignIn.signInSilently();
88 | }
89 | if (googleUser == null) {
90 | googleUser = await _googleSignIn.signIn();
91 | }
92 | return googleUser;
93 | }
94 |
95 | _handleAddWeightFromNotes(Store store, AddWeightFromNotes action) {
96 | if (store.state.firebaseState?.mainReference != null) {
97 | WeightEntry weightEntry =
98 | new WeightEntry(new DateTime.now(), action.weight, null);
99 | store.dispatch(new AddEntryAction(weightEntry));
100 | action = new AddWeightFromNotes(null);
101 | }
102 | }
103 |
104 | _handleGetSavedWeightNote(Store store) async {
105 | double savedWeight = await _getSavedWeightNote();
106 | if (savedWeight != null) {
107 | store.dispatch(new AddWeightFromNotes(savedWeight));
108 | }
109 | }
110 |
111 | Future _getSavedWeightNote() async {
112 | String sharedData = await const MethodChannel('app.channel.shared.data')
113 | .invokeMethod("getSavedNote");
114 | if (sharedData != null) {
115 | int firstIndex = sharedData.indexOf(new RegExp("[0-9]"));
116 | int lastIndex = sharedData.lastIndexOf(new RegExp("[0-9]"));
117 | if (firstIndex != -1) {
118 | String number = sharedData.substring(firstIndex, lastIndex + 1);
119 | double num = double.parse(number, (error) => null);
120 | return num;
121 | }
122 | }
123 | return null;
124 | }
125 |
126 | _handleAddedDatabaseReference(
127 | Store store, AddDatabaseReferenceAction action) {
128 | //maybe add cached entries
129 | if (action.cachedEntries?.isNotEmpty ?? false) {
130 | action.cachedEntries
131 | .forEach((entry) => store.dispatch(AddEntryAction(entry)));
132 | }
133 | //maybe add height from notes
134 | double weight = store.state.weightFromNotes;
135 | if (weight != null) {
136 | if (store.state.unit == 'lbs') {
137 | weight = weight / KG_LBS_RATIO;
138 | }
139 | if (weight >= MIN_KG_VALUE && weight <= MAX_KG_VALUE) {
140 | WeightEntry weightEntry =
141 | new WeightEntry(new DateTime.now(), weight, null);
142 | store.dispatch(new AddEntryAction(weightEntry));
143 | store.dispatch(new ConsumeWeightFromNotes());
144 | }
145 | }
146 | }
147 |
148 | _handleUserLoadedAction(Store store, UserLoadedAction action) {
149 | store.dispatch(new AddDatabaseReferenceAction(
150 | FirebaseDatabase.instance
151 | .reference()
152 | .child(store.state.firebaseState.firebaseUser.uid)
153 | .child("entries")
154 | ..onChildAdded
155 | .listen((event) => store.dispatch(new OnAddedAction(event)))
156 | ..onChildChanged
157 | .listen((event) => store.dispatch(new OnChangedAction(event)))
158 | ..onChildRemoved
159 | .listen((event) => store.dispatch(new OnRemovedAction(event))),
160 | cachedEntries: action.cachedEntries,
161 | ));
162 | }
163 |
164 | _handleSetUnitAction(SetUnitAction action, Store store) {
165 | _setUnit(action.unit)
166 | .then((nil) => store.dispatch(new OnUnitChangedAction(action.unit)));
167 | }
168 |
169 | _handleUndoRemovalAction(Store store) {
170 | WeightEntry lastRemovedEntry = store.state.removedEntryState.lastRemovedEntry;
171 | store.state.firebaseState.mainReference
172 | .child(lastRemovedEntry.key)
173 | .set(lastRemovedEntry.toJson());
174 | }
175 |
176 | _handleRemoveEntryAction(Store store, RemoveEntryAction action) {
177 | store.state.firebaseState.mainReference
178 | .child(action.weightEntry.key)
179 | .remove();
180 | }
181 |
182 | _handleEditEntryAction(Store store, EditEntryAction action) {
183 | store.state.firebaseState.mainReference
184 | .child(action.weightEntry.key)
185 | .set(action.weightEntry.toJson());
186 | }
187 |
188 | _handleAddEntryAction(Store store, AddEntryAction action) {
189 | store.state.firebaseState.mainReference
190 | .push()
191 | .set(action.weightEntry.toJson());
192 | }
193 |
194 | _handleInitAction(Store store) {
195 | _loadUnit().then((unit) => store.dispatch(new OnUnitChangedAction(unit)));
196 | if (store.state.firebaseState.firebaseUser == null) {
197 | FirebaseAuth.instance.currentUser().then((user) {
198 | if (user != null) {
199 | store.dispatch(new UserLoadedAction(user));
200 | } else {
201 | FirebaseAuth.instance
202 | .signInAnonymously()
203 | .then((user) => store.dispatch(new UserLoadedAction(user)));
204 | }
205 | });
206 | }
207 | }
208 |
209 | Future _setUnit(String unit) async {
210 | SharedPreferences prefs = await SharedPreferences.getInstance();
211 | prefs.setString('unit', unit);
212 | }
213 |
214 | Future _loadUnit() async {
215 | SharedPreferences prefs = await SharedPreferences.getInstance();
216 | return prefs.getString('unit') ?? 'kg';
217 | }
218 |
--------------------------------------------------------------------------------
/lib/logic/reducer.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_database/firebase_database.dart';
2 | import 'package:weight_tracker/logic/actions.dart';
3 | import 'package:weight_tracker/logic/redux_state.dart';
4 | import 'package:weight_tracker/model/weight_entry.dart';
5 |
6 | ReduxState reduce(ReduxState state, action) {
7 | List entries = _reduceEntries(state, action);
8 | String unit = _reduceUnit(state, action);
9 | RemovedEntryState removedEntryState = _reduceRemovedEntryState(state, action);
10 | WeightEntryDialogReduxState weightEntryDialogState =
11 | _reduceWeightEntryDialogState(state, action);
12 | FirebaseState firebaseState = _reduceFirebaseState(state, action);
13 | MainPageReduxState mainPageState = _reduceMainPageState(state, action);
14 | DateTime progressChartStartDate =
15 | _reduceProgressChartStartDate(state, action);
16 | double weightFromNotes = _reduceWeightFromNotes(state, action);
17 |
18 | return new ReduxState(
19 | entries: entries,
20 | unit: unit,
21 | removedEntryState: removedEntryState,
22 | weightEntryDialogState: weightEntryDialogState,
23 | firebaseState: firebaseState,
24 | mainPageState: mainPageState,
25 | progressChartStartDate: progressChartStartDate,
26 | weightFromNotes: weightFromNotes,
27 | );
28 | }
29 |
30 | double _reduceWeightFromNotes(ReduxState state, action) {
31 | double weight = state.weightFromNotes;
32 | if (action is AddWeightFromNotes) {
33 | weight = action.weight;
34 | } else if (action is ConsumeWeightFromNotes) {
35 | weight = null;
36 | }
37 | return weight;
38 | }
39 |
40 | String _reduceUnit(ReduxState reduxState, action) {
41 | String unit = reduxState.unit;
42 | if (action is OnUnitChangedAction) {
43 | unit = action.unit;
44 | }
45 | return unit;
46 | }
47 |
48 | MainPageReduxState _reduceMainPageState(ReduxState reduxState, action) {
49 | MainPageReduxState newMainPageState = reduxState.mainPageState;
50 | if (action is AcceptEntryAddedAction) {
51 | newMainPageState = newMainPageState.copyWith(hasEntryBeenAdded: false);
52 | } else if (action is OnAddedAction) {
53 | newMainPageState = newMainPageState.copyWith(hasEntryBeenAdded: true);
54 | }
55 | return newMainPageState;
56 | }
57 |
58 | FirebaseState _reduceFirebaseState(ReduxState reduxState, action) {
59 | FirebaseState newState = reduxState.firebaseState;
60 | if (action is InitAction) {
61 | FirebaseDatabase.instance.setPersistenceEnabled(true);
62 | } else if (action is UserLoadedAction) {
63 | newState = newState.copyWith(firebaseUser: action.firebaseUser);
64 | } else if (action is AddDatabaseReferenceAction) {
65 | newState = newState.copyWith(mainReference: action.databaseReference);
66 | }
67 | return newState;
68 | }
69 |
70 | RemovedEntryState _reduceRemovedEntryState(ReduxState reduxState, action) {
71 | RemovedEntryState newState = reduxState.removedEntryState;
72 | if (action is AcceptEntryRemovalAction) {
73 | newState = newState.copyWith(hasEntryBeenRemoved: false);
74 | } else if (action is OnRemovedAction) {
75 | newState = newState.copyWith(
76 | hasEntryBeenRemoved: true,
77 | lastRemovedEntry: new WeightEntry.fromSnapshot(action.event.snapshot));
78 | }
79 | return newState;
80 | }
81 |
82 | WeightEntryDialogReduxState _reduceWeightEntryDialogState(
83 | ReduxState reduxState, action) {
84 | WeightEntryDialogReduxState newState = reduxState.weightEntryDialogState;
85 | if (action is UpdateActiveWeightEntry) {
86 | newState = newState.copyWith(
87 | activeEntry: new WeightEntry.copy(action.weightEntry));
88 | } else if (action is OpenAddEntryDialog) {
89 | newState = newState.copyWith(
90 | activeEntry: new WeightEntry(
91 | new DateTime.now(),
92 | reduxState.entries.isEmpty ? 70.0 : reduxState.entries.first.weight,
93 | null),
94 | isEditMode: false);
95 | } else if (action is OpenEditEntryDialog) {
96 | newState =
97 | newState.copyWith(activeEntry: action.weightEntry, isEditMode: true);
98 | }
99 | return newState;
100 | }
101 |
102 | List _reduceEntries(ReduxState state, action) {
103 | List entries = new List.from(state.entries);
104 | if (action is OnAddedAction) {
105 | entries
106 | ..add(new WeightEntry.fromSnapshot(action.event.snapshot))
107 | ..sort((we1, we2) => we2.dateTime.compareTo(we1.dateTime));
108 | } else if (action is OnChangedAction) {
109 | WeightEntry newValue = new WeightEntry.fromSnapshot(action.event.snapshot);
110 | WeightEntry oldValue =
111 | entries.singleWhere((entry) => entry.key == newValue.key);
112 | entries
113 | ..[entries.indexOf(oldValue)] = newValue
114 | ..sort((we1, we2) => we2.dateTime.compareTo(we1.dateTime));
115 | } else if (action is OnRemovedAction) {
116 | WeightEntry removedEntry = state.entries
117 | .singleWhere((entry) => entry.key == action.event.snapshot.key);
118 | entries
119 | ..remove(removedEntry)
120 | ..sort((we1, we2) => we2.dateTime.compareTo(we1.dateTime));
121 | } else if (action is UserLoadedAction) {
122 | entries = [];
123 | }
124 | return entries;
125 | }
126 |
127 | DateTime _reduceProgressChartStartDate(ReduxState state, action) {
128 | DateTime date = state.progressChartStartDate;
129 | if (action is ChangeProgressChartStartDate) {
130 | date = action.dateTime;
131 | }
132 | return date;
133 | }
134 |
--------------------------------------------------------------------------------
/lib/logic/redux_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:firebase_database/firebase_database.dart';
3 | import 'package:meta/meta.dart';
4 | import 'package:weight_tracker/model/weight_entry.dart';
5 |
6 | @immutable
7 | class ReduxState {
8 | final List entries;
9 | final String unit;
10 | final RemovedEntryState removedEntryState;
11 | final WeightEntryDialogReduxState weightEntryDialogState;
12 | final FirebaseState firebaseState;
13 | final MainPageReduxState mainPageState;
14 | final DateTime progressChartStartDate;
15 | final double weightFromNotes;
16 |
17 | const ReduxState({
18 | this.firebaseState = const FirebaseState(),
19 | this.entries = const [],
20 | this.mainPageState = const MainPageReduxState(),
21 | this.unit = 'kg',
22 | this.removedEntryState = const RemovedEntryState(),
23 | this.weightEntryDialogState = const WeightEntryDialogReduxState(),
24 | this.progressChartStartDate,
25 | this.weightFromNotes,
26 | });
27 |
28 | ReduxState copyWith({
29 | FirebaseState firebaseState,
30 | List entries,
31 | bool hasEntryBeenAdded,
32 | String unit,
33 | RemovedEntryState removedEntryState,
34 | WeightEntryDialogReduxState weightEntryDialogState,
35 | DateTime progressChartStartDate,
36 | }) {
37 | return new ReduxState(
38 | firebaseState: firebaseState ?? this.firebaseState,
39 | entries: entries ?? this.entries,
40 | mainPageState: mainPageState ?? this.mainPageState,
41 | unit: unit ?? this.unit,
42 | weightEntryDialogState:
43 | weightEntryDialogState ?? this.weightEntryDialogState,
44 | removedEntryState: removedEntryState ?? this.removedEntryState,
45 | progressChartStartDate: progressChartStartDate ?? this.progressChartStartDate);
46 | }
47 | }
48 |
49 | @immutable
50 | class RemovedEntryState {
51 | final WeightEntry lastRemovedEntry;
52 | final bool hasEntryBeenRemoved; //in other words: should show snackbar?
53 |
54 | const RemovedEntryState(
55 | {this.lastRemovedEntry, this.hasEntryBeenRemoved = false});
56 |
57 | RemovedEntryState copyWith({
58 | WeightEntry lastRemovedEntry,
59 | bool hasEntryBeenRemoved,
60 | }) {
61 | return new RemovedEntryState(
62 | lastRemovedEntry: lastRemovedEntry ?? this.lastRemovedEntry,
63 | hasEntryBeenRemoved: hasEntryBeenRemoved ?? this.hasEntryBeenRemoved);
64 | }
65 | }
66 |
67 | @immutable
68 | class WeightEntryDialogReduxState {
69 | final bool isEditMode;
70 | final WeightEntry activeEntry; //entry to show in detail dialog
71 |
72 | const WeightEntryDialogReduxState({this.isEditMode, this.activeEntry});
73 |
74 | WeightEntryDialogReduxState copyWith({
75 | bool isEditMode,
76 | WeightEntry activeEntry,
77 | }) {
78 | return new WeightEntryDialogReduxState(
79 | isEditMode: isEditMode ?? this.isEditMode,
80 | activeEntry: activeEntry ?? this.activeEntry);
81 | }
82 | }
83 |
84 | @immutable
85 | class FirebaseState {
86 | final FirebaseUser firebaseUser;
87 | final DatabaseReference mainReference;
88 |
89 | const FirebaseState({this.firebaseUser, this.mainReference});
90 |
91 | FirebaseState copyWith({
92 | FirebaseUser firebaseUser,
93 | DatabaseReference mainReference,
94 | }) {
95 | return new FirebaseState(
96 | firebaseUser: firebaseUser ?? this.firebaseUser,
97 | mainReference: mainReference ?? this.mainReference);
98 | }
99 | }
100 |
101 | @immutable
102 | class MainPageReduxState {
103 | final bool hasEntryBeenAdded; //in other words: should scroll to top?
104 |
105 | const MainPageReduxState({this.hasEntryBeenAdded = false});
106 |
107 | MainPageReduxState copyWith({bool hasEntryBeenAdded}) {
108 | return new MainPageReduxState(
109 | hasEntryBeenAdded: hasEntryBeenAdded ?? this.hasEntryBeenAdded);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:redux/redux.dart';
4 | import 'package:weight_tracker/logic/actions.dart';
5 | import 'package:weight_tracker/logic/middleware.dart';
6 | import 'package:weight_tracker/logic/reducer.dart';
7 | import 'package:weight_tracker/logic/redux_state.dart';
8 | import 'package:weight_tracker/screens/main_page.dart';
9 | import 'package:firebase_analytics/firebase_analytics.dart';
10 | import 'package:firebase_analytics/observer.dart';
11 |
12 | void main() {
13 | runApp(new MyApp());
14 | }
15 |
16 | class MyApp extends StatelessWidget {
17 | final FirebaseAnalytics analytics = new FirebaseAnalytics();
18 | final Store store = new Store(reduce,
19 | initialState: new ReduxState(
20 | entries: [],
21 | unit: 'kg',
22 | removedEntryState: new RemovedEntryState(hasEntryBeenRemoved: false),
23 | firebaseState: new FirebaseState(),
24 | mainPageState: new MainPageReduxState(hasEntryBeenAdded: false),
25 | weightEntryDialogState: new WeightEntryDialogReduxState()),
26 | middleware: [middleware].toList());
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | store.dispatch(new InitAction());
31 | return new StoreProvider(
32 | store: store,
33 | child: new MaterialApp(
34 | title: 'Weight Tracker',
35 | theme: new ThemeData(
36 | primarySwatch: Colors.green,
37 | ),
38 | navigatorObservers: [
39 | FirebaseAnalyticsObserver(analytics: analytics),
40 | ],
41 | home: new MainPage(title: "Weight Tracker", analytics: analytics),
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/model/weight_entry.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_database/firebase_database.dart';
2 | import 'package:quiver/core.dart';
3 |
4 | class WeightEntry {
5 | String key;
6 | DateTime dateTime;
7 | double weight;
8 | String note;
9 |
10 | WeightEntry(this.dateTime, this.weight, this.note);
11 |
12 | WeightEntry.fromSnapshot(DataSnapshot snapshot)
13 | : key = snapshot.key,
14 | dateTime =
15 | new DateTime.fromMillisecondsSinceEpoch(snapshot.value["date"]),
16 | weight = snapshot.value["weight"].toDouble(),
17 | note = snapshot.value["note"];
18 |
19 | WeightEntry.copy(WeightEntry weightEntry)
20 | : key = weightEntry.key,
21 | //copy datetime
22 | dateTime = new DateTime.fromMillisecondsSinceEpoch(
23 | weightEntry.dateTime.millisecondsSinceEpoch),
24 | weight = weightEntry.weight,
25 | note = weightEntry.note;
26 |
27 | WeightEntry._internal(this.key, this.dateTime, this.weight, this.note);
28 |
29 | WeightEntry copyWith(
30 | {String key, DateTime dateTime, double weight, String note}) {
31 | return new WeightEntry._internal(
32 | key ?? this.key,
33 | dateTime ?? this.dateTime,
34 | weight ?? this.weight,
35 | note ?? this.note,
36 | );
37 | }
38 |
39 | toJson() {
40 | return {
41 | "weight": weight,
42 | "date": dateTime.millisecondsSinceEpoch,
43 | "note": note
44 | };
45 | }
46 |
47 | @override
48 | int get hashCode => hash4(key, dateTime, weight, note);
49 |
50 | @override
51 | bool operator ==(other) =>
52 | other is WeightEntry &&
53 | key == other.key &&
54 | dateTime.millisecondsSinceEpoch == other.dateTime
55 | .millisecondsSinceEpoch &&
56 | weight == other.weight &&
57 | note == other.note;
58 | }
59 |
--------------------------------------------------------------------------------
/lib/screens/history_page.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_redux/flutter_redux.dart';
5 | import 'package:meta/meta.dart';
6 | import 'package:weight_tracker/logic/actions.dart';
7 | import 'package:weight_tracker/logic/redux_state.dart';
8 | import 'package:weight_tracker/model/weight_entry.dart';
9 | import 'package:weight_tracker/screens/weight_entry_dialog.dart';
10 | import 'package:weight_tracker/widgets/weight_list_item.dart';
11 |
12 | @immutable
13 | class HistoryPageViewModel {
14 | final String unit;
15 | final List entries;
16 | final bool hasEntryBeenRemoved;
17 | final Function() acceptEntryRemoved;
18 | final Function() undoEntryRemoval;
19 | final Function(WeightEntry) openEditDialog;
20 |
21 | HistoryPageViewModel({
22 | this.undoEntryRemoval,
23 | this.hasEntryBeenRemoved,
24 | this.acceptEntryRemoved,
25 | this.entries,
26 | this.openEditDialog,
27 | this.unit,
28 | });
29 | }
30 |
31 | class HistoryPage extends StatelessWidget {
32 | HistoryPage({Key key, this.title}) : super(key: key);
33 | final String title;
34 |
35 | @override
36 | Widget build(BuildContext context) {
37 | return new StoreConnector(
38 | converter: (store) {
39 | return new HistoryPageViewModel(
40 | entries: store.state.entries,
41 | openEditDialog: (entry) {
42 | store.dispatch(new OpenEditEntryDialog(entry));
43 | Navigator.of(context).push(new MaterialPageRoute(
44 | builder: (BuildContext context) {
45 | return new WeightEntryDialog();
46 | },
47 | fullscreenDialog: true,
48 | ));
49 | },
50 | hasEntryBeenRemoved: store.state.removedEntryState
51 | .hasEntryBeenRemoved,
52 | acceptEntryRemoved: () =>
53 | store.dispatch(new AcceptEntryRemovalAction()),
54 | undoEntryRemoval: () => store.dispatch(new UndoRemovalAction()),
55 | unit: store.state.unit,
56 | );
57 | },
58 | builder: (context, viewModel) {
59 | if (viewModel.hasEntryBeenRemoved) {
60 | new Future.delayed(Duration.zero, () {
61 | Scaffold.of(context).showSnackBar(new SnackBar(
62 | content: new Text("Entry deleted."),
63 | action: new SnackBarAction(
64 | label: "UNDO",
65 | onPressed: () => viewModel.undoEntryRemoval(),
66 | ),
67 | ));
68 | viewModel.acceptEntryRemoved();
69 | });
70 | }
71 | if (viewModel.entries.isEmpty) {
72 | return new Center(
73 | child: new Text("Add your weight to see history"),
74 | );
75 | } else {
76 | return new ListView.builder(
77 | shrinkWrap: true,
78 | itemCount: viewModel.entries.length,
79 | itemBuilder: (buildContext, index) {
80 | //calculating difference
81 | double difference = index == viewModel.entries.length - 1
82 | ? 0.0
83 | : viewModel.entries[index].weight -
84 | viewModel.entries[index + 1].weight;
85 | return new InkWell(
86 | onTap: () =>
87 | viewModel.openEditDialog(viewModel.entries[index]),
88 | child: new WeightListItem(
89 | viewModel.entries[index], difference, viewModel.unit));
90 | },
91 | );
92 | }
93 | },
94 | );
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/screens/main_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_analytics/firebase_analytics.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_redux/flutter_redux.dart';
4 | import 'package:weight_tracker/logic/actions.dart';
5 | import 'package:weight_tracker/logic/redux_state.dart';
6 | import 'package:weight_tracker/screens/history_page.dart';
7 | import 'package:weight_tracker/screens/settings_screen.dart';
8 | import 'package:weight_tracker/screens/statistics_page.dart';
9 | import 'package:weight_tracker/screens/weight_entry_dialog.dart';
10 |
11 | class MainPageViewModel {
12 | final double defaultWeight;
13 | final bool hasEntryBeenAdded;
14 | final String unit;
15 | final Function() openAddEntryDialog;
16 | final Function() acceptEntryAddedCallback;
17 |
18 | MainPageViewModel({
19 | this.openAddEntryDialog,
20 | this.defaultWeight,
21 | this.hasEntryBeenAdded,
22 | this.acceptEntryAddedCallback,
23 | this.unit,
24 | });
25 | }
26 |
27 | class MainPage extends StatefulWidget {
28 | MainPage({Key key, this.title, this.analytics}) : super(key: key);
29 | final FirebaseAnalytics analytics;
30 | final String title;
31 |
32 | @override
33 | State createState() {
34 | return new MainPageState();
35 | }
36 | }
37 |
38 | class MainPageState extends State
39 | with SingleTickerProviderStateMixin {
40 | ScrollController _scrollViewController;
41 | TabController _tabController;
42 |
43 | @override
44 | void initState() {
45 | super.initState();
46 | _scrollViewController = new ScrollController();
47 | _tabController = new TabController(vsync: this, length: 2);
48 | }
49 |
50 | @override
51 | void dispose() {
52 | _scrollViewController.dispose();
53 | _tabController.dispose();
54 | super.dispose();
55 | }
56 |
57 | @override
58 | Widget build(BuildContext context) {
59 | return new StoreConnector(
60 | converter: (store) {
61 | return new MainPageViewModel(
62 | defaultWeight: store.state.entries.isEmpty
63 | ? 60.0
64 | : store.state.entries.first.weight,
65 | hasEntryBeenAdded: store.state.mainPageState.hasEntryBeenAdded,
66 | acceptEntryAddedCallback: () =>
67 | store.dispatch(new AcceptEntryAddedAction()),
68 | openAddEntryDialog: () {
69 | store.dispatch(new OpenAddEntryDialog());
70 | Navigator.of(context).push(new MaterialPageRoute(
71 | builder: (BuildContext context) {
72 | return new WeightEntryDialog();
73 | },
74 | fullscreenDialog: true,
75 | ));
76 | widget.analytics.logEvent(name: 'open_add_dialog');
77 | },
78 | unit: store.state.unit,
79 | );
80 | },
81 | onInit: (store) {
82 | store.dispatch(new GetSavedWeightNote());
83 | },
84 | builder: (context, viewModel) {
85 | if (viewModel.hasEntryBeenAdded) {
86 | _scrollToTop();
87 | viewModel.acceptEntryAddedCallback();
88 | }
89 | return new Scaffold(
90 | body: new NestedScrollView(
91 | controller: _scrollViewController,
92 | headerSliverBuilder:
93 | (BuildContext context, bool innerBoxIsScrolled) {
94 | return [
95 | new SliverAppBar(
96 | title: new Text(widget.title),
97 | pinned: true,
98 | floating: true,
99 | forceElevated: innerBoxIsScrolled,
100 | bottom: new TabBar(
101 | tabs: [
102 | new Tab(
103 | key: new Key('StatisticsTab'),
104 | text: "STATISTICS",
105 | icon: new Icon(Icons.show_chart),
106 | ),
107 | new Tab(
108 | key: new Key('HistoryTab'),
109 | text: "HISTORY",
110 | icon: new Icon(Icons.history),
111 | ),
112 | ],
113 | controller: _tabController,
114 | ),
115 | actions: _buildMenuActions(context),
116 | ),
117 | ];
118 | },
119 | body: new TabBarView(
120 | children: [
121 | new StatisticsPage(),
122 | new HistoryPage(),
123 | ],
124 | controller: _tabController,
125 | ),
126 | ),
127 | floatingActionButton: new FloatingActionButton(
128 | onPressed: () => viewModel.openAddEntryDialog(),
129 | tooltip: 'Add new weight entry',
130 | child: new Icon(Icons.add),
131 | ),
132 | );
133 | },
134 | );
135 | }
136 |
137 | List _buildMenuActions(BuildContext context) {
138 | return [
139 | IconButton(
140 | icon: new Icon(Icons.settings),
141 | onPressed: () => _openSettingsPage(context)),
142 | ];
143 | }
144 |
145 | _scrollToTop() {
146 | _scrollViewController.animateTo(
147 | 0.0,
148 | duration: const Duration(microseconds: 1),
149 | curve: new ElasticInCurve(0.01),
150 | );
151 | }
152 |
153 | _openSettingsPage(BuildContext context) async {
154 | Navigator.of(context).push(new MaterialPageRoute(
155 | builder: (BuildContext context) {
156 | return new SettingsPage();
157 | },
158 | ));
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/lib/screens/profile_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_redux/flutter_redux.dart';
4 | import 'package:weight_tracker/logic/actions.dart';
5 | import 'package:weight_tracker/logic/redux_state.dart';
6 |
7 | class ProfileView extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) {
10 | return new StoreConnector(
11 | converter: (store) {
12 | return new _ViewModel(
13 | user: store.state.firebaseState.firebaseUser,
14 | login: () => store
15 | .dispatch(LoginWithGoogle(cachedEntries: store.state.entries)),
16 | logout: () => store.dispatch(LogoutAction()),
17 | );
18 | },
19 | builder: (BuildContext context, _ViewModel vm) {
20 | return (vm.user?.isAnonymous ?? true)
21 | ? _anonymousView(context, vm)
22 | : _loggedInView(context, vm);
23 | },
24 | );
25 | }
26 |
27 | Widget _loggedInView(BuildContext context, _ViewModel vm) {
28 | return Column(
29 | children: [
30 | _drawAvatar(NetworkImage(vm.user.photoUrl)),
31 | _drawLabel(context, vm.user.displayName),
32 | Text(vm.user.email),
33 | Padding(
34 | padding: const EdgeInsets.symmetric(vertical: 16.0),
35 | child: Container(
36 | width: 120.0,
37 | child: RaisedButton(
38 | color: Colors.green,
39 | child: Text(
40 | "Logout",
41 | style: TextStyle(color: Colors.white),
42 | ),
43 | onPressed: vm.logout,
44 | ),
45 | ),
46 | )
47 | ],
48 | );
49 | }
50 |
51 | Widget _anonymousView(BuildContext context, _ViewModel vm) {
52 | return Column(
53 | children: [
54 | _drawAvatar(AssetImage('assets/user.png')),
55 | _drawLabel(context, 'Anonymous user'),
56 | Padding(
57 | padding: const EdgeInsets.symmetric(horizontal: 16.0),
58 | child: Text(
59 | 'To synchronize your data across all devices link your data with a Google account.',
60 | textAlign: TextAlign.center,
61 | ),
62 | ),
63 | Padding(
64 | padding: const EdgeInsets.symmetric(vertical: 16.0),
65 | child: OAuthLoginButton(
66 | onPressed: vm.login,
67 | text: 'Continue with Google',
68 | assetName: 'assets/google.png',
69 | backgroundColor: Colors.white,
70 | ),
71 | ),
72 | ],
73 | );
74 | }
75 |
76 | Padding _drawLabel(BuildContext context, String label) {
77 | return Padding(
78 | padding: const EdgeInsets.all(16.0),
79 | child: Text(
80 | label,
81 | style: Theme.of(context).textTheme.display1,
82 | ),
83 | );
84 | }
85 |
86 | Padding _drawAvatar(ImageProvider imageProvider) {
87 | return Padding(
88 | padding: const EdgeInsets.only(top: 16.0),
89 | child: CircleAvatar(
90 | backgroundImage: imageProvider,
91 | backgroundColor: Colors.white10,
92 | radius: 48.0,
93 | ),
94 | );
95 | }
96 | }
97 |
98 | class _ViewModel {
99 | final FirebaseUser user;
100 | final Function() login;
101 | final Function() logout;
102 |
103 | _ViewModel({
104 | @required this.user,
105 | @required this.login,
106 | @required this.logout,
107 | });
108 | }
109 |
110 | class OAuthLoginButton extends StatelessWidget {
111 | final Function() onPressed;
112 | final String text;
113 | final String assetName;
114 | final Color backgroundColor;
115 |
116 | OAuthLoginButton(
117 | {@required this.onPressed,
118 | @required this.text,
119 | @required this.assetName,
120 | @required this.backgroundColor});
121 |
122 | @override
123 | Widget build(BuildContext context) {
124 | return new Container(
125 | width: 240.0,
126 | child: new RaisedButton(
127 | color: backgroundColor,
128 | onPressed: onPressed,
129 | padding: new EdgeInsets.only(right: 8.0),
130 | child: new Row(
131 | children: [
132 | Padding(
133 | padding: const EdgeInsets.all(8.0),
134 | child: new Image.asset(
135 | assetName,
136 | height: 30.0,
137 | ),
138 | ),
139 | new Expanded(
140 | child: new Padding(
141 | padding: const EdgeInsets.only(left: 8.0),
142 | child: new Text(
143 | text,
144 | style: Theme.of(context).textTheme.button,
145 | ),
146 | )),
147 | ],
148 | ),
149 | ),
150 | );
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/lib/screens/settings_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:meta/meta.dart';
4 | import 'package:weight_tracker/logic/actions.dart';
5 | import 'package:weight_tracker/logic/redux_state.dart';
6 |
7 | @immutable
8 | class SettingsPageViewModel {
9 | final String unit;
10 | final Function(String) onUnitChanged;
11 |
12 | SettingsPageViewModel({this.unit, this.onUnitChanged});
13 | }
14 |
15 | class SettingsPage extends StatelessWidget {
16 | @override
17 | Widget build(BuildContext context) {
18 | return new StoreConnector(
19 | converter: (store) {
20 | return new SettingsPageViewModel(
21 | unit: store.state.unit,
22 | onUnitChanged: (newUnit) => store.dispatch(new SetUnitAction(newUnit)),
23 | );
24 | }, builder: (context, viewModel) {
25 | return new Scaffold(
26 | appBar: new AppBar(
27 | title: new Text("Settings"),
28 | ),
29 | body: Column(
30 | children: [
31 | new Padding(
32 | padding: new EdgeInsets.all(16.0),
33 | child: _unitView(context, viewModel),
34 | ),
35 | // ProfileView(),
36 | ],
37 | ),
38 | );
39 | });
40 | }
41 |
42 | Row _unitView(BuildContext context, SettingsPageViewModel viewModel) {
43 | return new Row(
44 | children: [
45 | new Expanded(
46 | child: new Text(
47 | "Unit",
48 | style: Theme.of(context).textTheme.headline,
49 | )),
50 | new DropdownButton(
51 | key: const Key('UnitDropdown'),
52 | value: viewModel.unit,
53 | items: ["kg", "lbs"].map((String value) {
54 | return new DropdownMenuItem(
55 | value: value,
56 | child: new Text(value),
57 | );
58 | }).toList(),
59 | onChanged: (newUnit) => viewModel.onUnitChanged(newUnit),
60 | ),
61 | ],
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/screens/statistics_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:weight_tracker/logic/actions.dart';
4 | import 'package:weight_tracker/logic/constants.dart';
5 | import 'package:weight_tracker/logic/redux_state.dart';
6 | import 'package:weight_tracker/model/weight_entry.dart';
7 | import 'package:weight_tracker/screens/weight_entry_dialog.dart';
8 | import 'package:weight_tracker/widgets/progress_chart.dart';
9 |
10 | class _StatisticsPageViewModel {
11 | final double totalProgress;
12 | final double currentWeight;
13 | final double last7daysProgress;
14 | final double last30daysProgress;
15 | final List entries;
16 | final String unit;
17 | final Function() openAddEntryDialog;
18 |
19 | _StatisticsPageViewModel({
20 | this.last7daysProgress,
21 | this.last30daysProgress,
22 | this.totalProgress,
23 | this.currentWeight,
24 | this.entries,
25 | this.unit,
26 | this.openAddEntryDialog,
27 | });
28 | }
29 |
30 | class StatisticsPage extends StatelessWidget {
31 | @override
32 | Widget build(BuildContext context) {
33 | return new StoreConnector(
34 | converter: (store) {
35 | String unit = store.state.unit;
36 | List entries = new List();
37 | store.state.entries.forEach((entry) {
38 | if (unit == "kg") {
39 | entries.add(entry);
40 | } else {
41 | entries.add(entry.copyWith(weight: entry.weight * KG_LBS_RATIO));
42 | }
43 | });
44 | List last7daysEntries = entries
45 | .where((entry) =>
46 | entry.dateTime
47 | .isAfter(new DateTime.now().subtract(new Duration(days: 7))))
48 | .toList();
49 | List last30daysEntries = entries
50 | .where((entry) =>
51 | entry.dateTime
52 | .isAfter(new DateTime.now().subtract(new Duration(days: 30))))
53 | .toList();
54 | return new _StatisticsPageViewModel(
55 | totalProgress: entries.isEmpty
56 | ? 0.0
57 | : (entries.first.weight - entries.last.weight),
58 | currentWeight: entries.isEmpty ? 0.0 : entries.first.weight,
59 | last7daysProgress: last7daysEntries.isEmpty
60 | ? 0.0
61 | : (last7daysEntries.first.weight - last7daysEntries.last.weight),
62 | last30daysProgress: last30daysEntries.isEmpty
63 | ? 0.0
64 | : (last30daysEntries.first.weight -
65 | last30daysEntries.last.weight),
66 | entries: entries,
67 | unit: unit,
68 | openAddEntryDialog: () {
69 | if (last30daysEntries.isEmpty) {
70 | store.dispatch(new OpenAddEntryDialog());
71 | Navigator.of(context).push(new MaterialPageRoute(
72 | builder: (BuildContext context) {
73 | return new WeightEntryDialog();
74 | },
75 | fullscreenDialog: true,
76 | ));
77 | }
78 | },
79 | );
80 | },
81 | builder: (context, viewModel) {
82 | return new ListView(
83 | children: [
84 | new GestureDetector(
85 | onTap: viewModel.openAddEntryDialog,
86 | child: new _StatisticCardWrapper(
87 | child: new Padding(
88 | padding: new EdgeInsets.all(8.0),
89 | child: new ProgressChart()),
90 | height: 250.0,
91 | ),
92 | ),
93 | new _StatisticCard(
94 | title: "Current weight",
95 | value: viewModel.currentWeight,
96 | unit: viewModel.unit,
97 | ),
98 | new _StatisticCard(
99 | title: "Progress done",
100 | value: viewModel.totalProgress,
101 | processNumberSymbol: true,
102 | unit: viewModel.unit,
103 | ),
104 | new Row(
105 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
106 | children: [
107 | new Expanded(
108 | child: new _StatisticCard(
109 | title: "Last week",
110 | value: viewModel.last7daysProgress,
111 | textSizeFactor: 0.8,
112 | processNumberSymbol: true,
113 | unit: viewModel.unit,
114 | ),
115 | ),
116 | new Expanded(
117 | child: new _StatisticCard(
118 | title: "Last month",
119 | value: viewModel.last30daysProgress,
120 | textSizeFactor: 0.8,
121 | processNumberSymbol: true,
122 | unit: viewModel.unit,
123 | ),
124 | ),
125 | ],
126 | )
127 | ],
128 | );
129 | },
130 | );
131 | }
132 | }
133 |
134 | class _StatisticCardWrapper extends StatelessWidget {
135 | final double height;
136 | final Widget child;
137 |
138 | _StatisticCardWrapper({this.height = 120.0, this.child});
139 |
140 | @override
141 | Widget build(BuildContext context) {
142 | return new Row(
143 | children: [
144 | new Expanded(
145 | child: new Container(
146 | height: height,
147 | child: new Card(child: child),
148 | ),
149 | ),
150 | ],
151 | );
152 | }
153 | }
154 |
155 | class _StatisticCard extends StatelessWidget {
156 | final String title;
157 | final num value;
158 | final bool processNumberSymbol;
159 | final double textSizeFactor;
160 | final String unit;
161 |
162 | _StatisticCard({this.title,
163 | this.value,
164 | this.unit,
165 | this.processNumberSymbol = false,
166 | this.textSizeFactor = 1.0});
167 |
168 | @override
169 | Widget build(BuildContext context) {
170 | Color numberColor =
171 | (processNumberSymbol && value > 0) ? Colors.red : Colors.green;
172 | String numberSymbol = processNumberSymbol && value > 0 ? "+" : "";
173 | return new _StatisticCardWrapper(
174 | child: new Column(
175 | children: [
176 | new Expanded(
177 | child: new Row(
178 | children: [
179 | new Text(
180 | numberSymbol + value.toStringAsFixed(1),
181 | textScaleFactor: textSizeFactor,
182 | style: Theme
183 | .of(context)
184 | .textTheme
185 | .display2
186 | .copyWith(color: numberColor),
187 | ),
188 | new Padding(
189 | padding: new EdgeInsets.only(left: 5.0),
190 | child: new Text(unit)),
191 | ],
192 | mainAxisAlignment: MainAxisAlignment.center,
193 | ),
194 | ),
195 | new Padding(
196 | child: new Text(title),
197 | padding: new EdgeInsets.only(bottom: 8.0),
198 | ),
199 | ],
200 | ),
201 | );
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/lib/screens/weight_entry_dialog.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_redux/flutter_redux.dart';
5 | import 'package:intl/intl.dart';
6 | import 'package:meta/meta.dart';
7 | import 'package:numberpicker/numberpicker.dart';
8 | import 'package:weight_tracker/logic/actions.dart';
9 | import 'package:weight_tracker/logic/constants.dart';
10 | import 'package:weight_tracker/logic/redux_state.dart';
11 | import 'package:weight_tracker/model/weight_entry.dart';
12 |
13 | class DialogViewModel {
14 | final WeightEntry weightEntry;
15 | final String unit;
16 | final bool isEditMode;
17 | final double weightToDisplay;
18 | final Function(WeightEntry) onEntryChanged;
19 | final Function() onDeletePressed;
20 | final Function() onSavePressed;
21 |
22 | DialogViewModel({
23 | this.weightEntry,
24 | this.unit,
25 | this.isEditMode,
26 | this.weightToDisplay,
27 | this.onEntryChanged,
28 | this.onDeletePressed,
29 | this.onSavePressed,
30 | });
31 | }
32 |
33 | class WeightEntryDialog extends StatefulWidget {
34 | @override
35 | State createState() {
36 | return new WeightEntryDialogState();
37 | }
38 | }
39 |
40 | class WeightEntryDialogState extends State {
41 | TextEditingController _textController;
42 | bool wasBuiltOnce = false;
43 |
44 | @override
45 | void initState() {
46 | super.initState();
47 | _textController = new TextEditingController();
48 | }
49 |
50 | @override
51 | Widget build(BuildContext context) {
52 | return new StoreConnector(
53 | converter: (store) {
54 | WeightEntry activeEntry =
55 | store.state.weightEntryDialogState.activeEntry;
56 | return new DialogViewModel(
57 | weightEntry: activeEntry,
58 | unit: store.state.unit,
59 | isEditMode: store.state.weightEntryDialogState.isEditMode,
60 | weightToDisplay: store.state.unit == "kg"
61 | ? activeEntry.weight
62 | : double.parse(
63 | (activeEntry.weight * KG_LBS_RATIO).toStringAsFixed(1)),
64 | onEntryChanged: (entry) =>
65 | store.dispatch(new UpdateActiveWeightEntry(entry)),
66 | onDeletePressed: () {
67 | store.dispatch(new RemoveEntryAction(activeEntry));
68 | Navigator.of(context).pop();
69 | },
70 | onSavePressed: () {
71 | if (store.state.weightEntryDialogState.isEditMode) {
72 | store.dispatch(new EditEntryAction(activeEntry));
73 | } else {
74 | store.dispatch(new AddEntryAction(activeEntry));
75 | }
76 | Navigator.of(context).pop();
77 | });
78 | },
79 | builder: (context, viewModel) {
80 | if (!wasBuiltOnce) {
81 | wasBuiltOnce = true;
82 | _textController.text = viewModel.weightEntry.note;
83 | }
84 | return new Scaffold(
85 | appBar: _createAppBar(context, viewModel),
86 | body: new Column(
87 | children: [
88 | new ListTile(
89 | leading: new Icon(Icons.today, color: Colors.grey[500]),
90 | title: new DateTimeItem(
91 | dateTime: viewModel.weightEntry.dateTime,
92 | onChanged: (dateTime) =>
93 | viewModel.onEntryChanged(
94 | viewModel.weightEntry..dateTime = dateTime),
95 | ),
96 | ),
97 | new ListTile(
98 | leading: new Image.asset(
99 | "assets/scale-bathroom.png",
100 | color: Colors.grey[500],
101 | height: 24.0,
102 | width: 24.0,
103 | ),
104 | title: new Text(
105 | viewModel.weightToDisplay.toStringAsFixed(1) +
106 | " " +
107 | viewModel.unit,
108 | ),
109 | onTap: () => _showWeightPicker(context, viewModel),
110 | ),
111 | new ListTile(
112 | leading: new Icon(Icons.speaker_notes, color: Colors.grey[500]),
113 | title: new TextField(
114 | decoration: new InputDecoration(
115 | hintText: 'Optional note',
116 | ),
117 | controller: _textController,
118 | onChanged: (value) {
119 | viewModel
120 | .onEntryChanged(viewModel.weightEntry..note = value);
121 | }),
122 | ),
123 | ],
124 | ),
125 | );
126 | },
127 | );
128 | }
129 |
130 | Widget _createAppBar(BuildContext context, DialogViewModel viewModel) {
131 | TextStyle actionStyle =
132 | Theme
133 | .of(context)
134 | .textTheme
135 | .subhead
136 | .copyWith(color: Colors.white);
137 | Text title = viewModel.isEditMode
138 | ? const Text("Edit entry")
139 | : const Text("New entry");
140 | List actions = [];
141 | if (viewModel.isEditMode) {
142 | actions.add(
143 | new FlatButton(
144 | onPressed: viewModel.onDeletePressed,
145 | child: new Text(
146 | 'DELETE',
147 | style: actionStyle,
148 | ),
149 | ),
150 | );
151 | }
152 | actions.add(new FlatButton(
153 | onPressed: viewModel.onSavePressed,
154 | child: new Text(
155 | 'SAVE',
156 | style: actionStyle,
157 | ),
158 | ));
159 |
160 | return new AppBar(
161 | title: title,
162 | actions: actions,
163 | );
164 | }
165 |
166 | _showWeightPicker(BuildContext context, DialogViewModel viewModel) {
167 | showDialog(
168 | context: context,
169 | builder: (context) =>
170 | new NumberPickerDialog.decimal(
171 | minValue: viewModel.unit == "kg"
172 | ? MIN_KG_VALUE
173 | : (MIN_KG_VALUE * KG_LBS_RATIO).toInt(),
174 | maxValue: viewModel.unit == "kg"
175 | ? MAX_KG_VALUE
176 | : (MAX_KG_VALUE * KG_LBS_RATIO).toInt(),
177 | initialDoubleValue: viewModel.weightToDisplay,
178 | title: new Text("Enter your weight"),
179 | ),
180 | ).then((double value) {
181 | if (value != null) {
182 | if (viewModel.unit == "lbs") {
183 | value = value / KG_LBS_RATIO;
184 | }
185 | viewModel.onEntryChanged(viewModel.weightEntry..weight = value);
186 | }
187 | });
188 | }
189 | }
190 |
191 | class DateTimeItem extends StatelessWidget {
192 | DateTimeItem({Key key, DateTime dateTime, @required this.onChanged})
193 | : assert(onChanged != null),
194 | date = dateTime == null
195 | ? new DateTime.now()
196 | : new DateTime(dateTime.year, dateTime.month, dateTime.day),
197 | time = dateTime == null
198 | ? new DateTime.now()
199 | : new TimeOfDay(hour: dateTime.hour, minute: dateTime.minute),
200 | super(key: key);
201 |
202 | final DateTime date;
203 | final TimeOfDay time;
204 | final ValueChanged onChanged;
205 |
206 | @override
207 | Widget build(BuildContext context) {
208 | return new Row(
209 | children: [
210 | new Expanded(
211 | child: new InkWell(
212 | key: new Key('CalendarItem'),
213 | onTap: (() => _showDatePicker(context)),
214 | child: new Padding(
215 | padding: new EdgeInsets.symmetric(vertical: 8.0),
216 | child: new Text(new DateFormat('EEEE, MMMM d').format(date))),
217 | ),
218 | ),
219 | new InkWell(
220 | key: new Key('TimeItem'),
221 | onTap: (() => _showTimePicker(context)),
222 | child: new Padding(
223 | padding: new EdgeInsets.symmetric(vertical: 8.0),
224 | child: new Text(time.format(context))),
225 | ),
226 | ],
227 | );
228 | }
229 |
230 | Future _showDatePicker(BuildContext context) async {
231 | DateTime dateTimePicked = await showDatePicker(
232 | context: context,
233 | initialDate: date,
234 | firstDate: date.subtract(const Duration(days: 365)),
235 | lastDate: new DateTime.now());
236 |
237 | if (dateTimePicked != null) {
238 | onChanged(new DateTime(dateTimePicked.year, dateTimePicked.month,
239 | dateTimePicked.day, time.hour, time.minute));
240 | }
241 | }
242 |
243 | Future _showTimePicker(BuildContext context) async {
244 | TimeOfDay timeOfDay =
245 | await showTimePicker(context: context, initialTime: time);
246 |
247 | if (timeOfDay != null) {
248 | onChanged(new DateTime(
249 | date.year, date.month, date.day, timeOfDay.hour, timeOfDay.minute));
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/lib/widgets/progress_chart.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as math;
2 | import 'dart:ui' as ui;
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_redux/flutter_redux.dart';
6 | import 'package:intl/intl.dart';
7 | import 'package:tuple/tuple.dart';
8 | import 'package:weight_tracker/logic/actions.dart';
9 | import 'package:weight_tracker/logic/constants.dart';
10 | import 'package:weight_tracker/logic/redux_state.dart';
11 | import 'package:weight_tracker/model/weight_entry.dart';
12 | import 'package:weight_tracker/widgets/progress_chart_dropdown.dart';
13 | import 'package:weight_tracker/widgets/progress_chart_utils.dart' as utils;
14 | import 'package:weight_tracker/widgets/progress_chart_utils.dart';
15 |
16 | class ProgressChartViewModel {
17 | final List allEntries;
18 | final String unit;
19 |
20 | ProgressChartViewModel({
21 | this.allEntries,
22 | this.unit,
23 | });
24 | }
25 |
26 | class ProgressChart extends StatefulWidget {
27 | @override
28 | ProgressChartState createState() {
29 | return new ProgressChartState();
30 | }
31 | }
32 |
33 | class ProgressChartState extends State {
34 | DateTime startDate;
35 | DateTime snapShotStartDate;
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return new StoreConnector(
40 | converter: (store) {
41 | return new ProgressChartViewModel(
42 | allEntries: store.state.entries,
43 | unit: store.state.unit,
44 | );
45 | },
46 | onInit: (store) {
47 | this.startDate = store.state.progressChartStartDate ??
48 | DateTime.now().subtract(Duration(days: 30));
49 | },
50 | onDispose: (store) {
51 | store.dispatch(ChangeProgressChartStartDate(this.startDate));
52 | },
53 | builder: _buildChartWithDropdown,
54 | );
55 | }
56 |
57 | Widget _buildChart(ProgressChartViewModel viewModel) {
58 | return GestureDetector(
59 | onScaleStart: _onScaleStart,
60 | onScaleUpdate: _onScaleUpdate,
61 | child: CustomPaint(
62 | painter: ChartPainter(
63 | utils.prepareEntryList(viewModel.allEntries, startDate),
64 | daysToDraw(startDate),
65 | viewModel.unit == "lbs",
66 | ),
67 | ),
68 | );
69 | }
70 |
71 | Widget _buildChartWithDropdown(
72 | BuildContext context, ProgressChartViewModel viewModel) {
73 | return new Column(
74 | crossAxisAlignment: CrossAxisAlignment.stretch,
75 | mainAxisSize: MainAxisSize.max,
76 | children: [
77 | new Expanded(child: _buildChart(viewModel)),
78 | ProgressChartDropdown(
79 | daysToShow: daysToDraw(startDate),
80 | onStartSelected: (date) => setState(() => startDate = date),
81 | ),
82 | ],
83 | );
84 | }
85 |
86 | void _onScaleStart(ScaleStartDetails details) {
87 | setState(() {
88 | this.snapShotStartDate = this.startDate;
89 | });
90 | }
91 |
92 | void _onScaleUpdate(ScaleUpdateDetails details) {
93 | int previousNumberOfDays = daysToDraw(snapShotStartDate);
94 | int newNumberOfDays = (previousNumberOfDays / details.scale).round();
95 | if (newNumberOfDays >= 7) {
96 | setState(() {
97 | startDate =
98 | new DateTime.now().subtract(Duration(days: newNumberOfDays - 1));
99 | });
100 | }
101 | }
102 |
103 | int daysToDraw(DateTime date) {
104 | DateTime now = copyDateWithoutTime(new DateTime.now());
105 | DateTime start = copyDateWithoutTime(date);
106 | return now.difference(start).inDays + 1;
107 | }
108 | }
109 |
110 | class ChartPainter extends CustomPainter {
111 | final List entries;
112 | final int numberOfDays;
113 | final bool isLbs;
114 |
115 | ChartPainter(this.entries, this.numberOfDays, this.isLbs);
116 |
117 | double leftOffsetStart;
118 | double topOffsetEnd;
119 | double drawingWidth;
120 | double drawingHeight;
121 |
122 | static const int NUMBER_OF_HORIZONTAL_LINES = 5;
123 |
124 | @override
125 | void paint(Canvas canvas, Size size) {
126 | leftOffsetStart = size.width * 0.07;
127 | topOffsetEnd = size.height * 0.9;
128 | drawingWidth = size.width * 0.93;
129 | drawingHeight = topOffsetEnd;
130 |
131 | if (entries.isEmpty) {
132 | _drawParagraphInsteadOfChart(
133 | canvas, size, "Add your current weight to see history");
134 | } else {
135 | Tuple2 borderLineValues = _getMinAndMaxValues(entries, isLbs);
136 | _drawHorizontalLinesAndLabels(
137 | canvas, size, borderLineValues.item1, borderLineValues.item2);
138 | _drawBottomLabels(canvas, size);
139 |
140 | _drawLines(canvas, borderLineValues.item1, borderLineValues.item2, isLbs);
141 | }
142 | }
143 |
144 | @override
145 | bool shouldRepaint(ChartPainter old) => true;
146 |
147 | ///draws actual chart
148 | void _drawLines(
149 | ui.Canvas canvas, int minLineValue, int maxLineValue, bool isLbs) {
150 | final paint = new Paint()
151 | ..color = Colors.blue[400]
152 | ..strokeWidth = 3.0;
153 | DateTime beginningOfChart =
154 | utils.getStartDateOfChart(new DateTime.now(), numberOfDays);
155 | for (int i = 0; i < entries.length - 1; i++) {
156 | Offset startEntryOffset = _getEntryOffset(
157 | entries[i], beginningOfChart, minLineValue, maxLineValue, isLbs);
158 | Offset endEntryOffset = _getEntryOffset(
159 | entries[i + 1], beginningOfChart, minLineValue, maxLineValue, isLbs);
160 | canvas.drawLine(startEntryOffset, endEntryOffset, paint);
161 | canvas.drawCircle(endEntryOffset, 3.0, paint);
162 | }
163 | canvas.drawCircle(
164 | _getEntryOffset(
165 | entries.first, beginningOfChart, minLineValue, maxLineValue, isLbs),
166 | 5.0,
167 | paint);
168 | }
169 |
170 | /// Draws horizontal lines and labels informing about weight values attached to those lines
171 | void _drawHorizontalLinesAndLabels(
172 | Canvas canvas, Size size, int minLineValue, int maxLineValue) {
173 | final paint = new Paint()..color = Colors.grey[300];
174 | int lineStep = _calculateHorizontalLineStep(maxLineValue, minLineValue);
175 | double offsetStep = _calculateHorizontalOffsetStep;
176 | for (int line = 0; line < NUMBER_OF_HORIZONTAL_LINES; line++) {
177 | double yOffset = line * offsetStep;
178 | _drawHorizontalLabel(maxLineValue, line, lineStep, canvas, yOffset);
179 | _drawHorizontalLine(canvas, yOffset, size, paint);
180 | }
181 | }
182 |
183 | void _drawHorizontalLine(
184 | ui.Canvas canvas, double yOffset, ui.Size size, ui.Paint paint) {
185 | canvas.drawLine(
186 | new Offset(leftOffsetStart, 5 + yOffset),
187 | new Offset(size.width, 5 + yOffset),
188 | paint,
189 | );
190 | }
191 |
192 | void _drawHorizontalLabel(int maxLineValue, int line, int lineStep,
193 | ui.Canvas canvas, double yOffset) {
194 | ui.Paragraph paragraph =
195 | _buildParagraphForLeftLabel(maxLineValue, line, lineStep);
196 | canvas.drawParagraph(
197 | paragraph,
198 | new Offset(0.0, yOffset),
199 | );
200 | }
201 |
202 | /// Calculates offset difference between horizontal lines.
203 | ///
204 | /// e.g. between every line should be 100px space.
205 | double get _calculateHorizontalOffsetStep {
206 | return drawingHeight / (NUMBER_OF_HORIZONTAL_LINES - 1);
207 | }
208 |
209 | /// Calculates weight difference between horizontal lines.
210 | ///
211 | /// e.g. every line should increment weight by 5
212 | int _calculateHorizontalLineStep(int maxLineValue, int minLineValue) {
213 | return (maxLineValue - minLineValue) ~/ (NUMBER_OF_HORIZONTAL_LINES - 1);
214 | }
215 |
216 | void _drawBottomLabels(Canvas canvas, Size size) {
217 | for (int daysFromStart = numberOfDays;
218 | daysFromStart > 0;
219 | daysFromStart = (daysFromStart - (numberOfDays / 4)).round()) {
220 | double offsetXbyDay = drawingWidth / numberOfDays;
221 | double offsetX = leftOffsetStart + offsetXbyDay * daysFromStart;
222 | ui.Paragraph paragraph = _buildParagraphForBottomLabel(daysFromStart);
223 | canvas.drawParagraph(
224 | paragraph,
225 | new Offset(offsetX - 50.0, 10.0 + drawingHeight),
226 | );
227 | }
228 | }
229 |
230 | ///Builds paragraph for label placed on the bottom (dates)
231 | ui.Paragraph _buildParagraphForBottomLabel(int daysFromStart) {
232 | ui.ParagraphBuilder builder = new ui.ParagraphBuilder(
233 | new ui.ParagraphStyle(fontSize: 10.0, textAlign: TextAlign.right))
234 | ..pushStyle(new ui.TextStyle(color: Colors.black))
235 | ..addText(new DateFormat('d MMM').format(new DateTime.now()
236 | .subtract(new Duration(days: numberOfDays - daysFromStart))));
237 | final ui.Paragraph paragraph = builder.build()
238 | ..layout(new ui.ParagraphConstraints(width: 50.0));
239 | return paragraph;
240 | }
241 |
242 | ///Builds text paragraph for label placed on the left side of a chart (weights)
243 | ui.Paragraph _buildParagraphForLeftLabel(
244 | int maxLineValue, int line, int lineStep) {
245 | ui.ParagraphBuilder builder = new ui.ParagraphBuilder(
246 | new ui.ParagraphStyle(
247 | fontSize: 10.0,
248 | textAlign: TextAlign.right,
249 | ),
250 | )
251 | ..pushStyle(new ui.TextStyle(color: Colors.black))
252 | ..addText((maxLineValue - line * lineStep).toString());
253 | final ui.Paragraph paragraph = builder.build()
254 | ..layout(new ui.ParagraphConstraints(width: leftOffsetStart - 4));
255 | return paragraph;
256 | }
257 |
258 | ///Produces minimal and maximal value of horizontal line that will be displayed
259 | Tuple2 _getMinAndMaxValues(List entries, bool isLbs) {
260 | double maxWeight = entries.map((entry) => entry.weight).reduce(math.max);
261 | double minWeight = entries.map((entry) => entry.weight).reduce(math.min);
262 |
263 | if (isLbs) {
264 | maxWeight *= KG_LBS_RATIO;
265 | minWeight *= KG_LBS_RATIO;
266 | }
267 | int maxLineValue;
268 | int minLineValue;
269 |
270 | if (maxWeight == minWeight) {
271 | maxLineValue = maxWeight.ceil() + 1;
272 | minLineValue = maxLineValue - 4;
273 | } else {
274 | maxLineValue = maxWeight.ceil();
275 | int difference = maxLineValue - minWeight.floor();
276 | int toSubtract = (NUMBER_OF_HORIZONTAL_LINES - 1) -
277 | (difference % (NUMBER_OF_HORIZONTAL_LINES - 1));
278 | if (toSubtract == NUMBER_OF_HORIZONTAL_LINES - 1) {
279 | toSubtract = 0;
280 | }
281 | minLineValue = minWeight.floor() - toSubtract;
282 | }
283 | return new Tuple2(minLineValue, maxLineValue);
284 | }
285 |
286 | /// Calculates offset at which given entry should be painted
287 | Offset _getEntryOffset(WeightEntry entry, DateTime beginningOfChart,
288 | int minLineValue, int maxLineValue, bool isLbs) {
289 | double entryWeightToShow =
290 | isLbs ? entry.weight * KG_LBS_RATIO : entry.weight;
291 | int daysFromBeginning = entry.dateTime.difference(beginningOfChart).inDays;
292 | double relativeXposition = daysFromBeginning / (numberOfDays - 1);
293 | double xOffset = leftOffsetStart + relativeXposition * drawingWidth;
294 | double relativeYposition =
295 | (entryWeightToShow - minLineValue) / (maxLineValue - minLineValue);
296 | double yOffset = 5 + drawingHeight - relativeYposition * drawingHeight;
297 | return new Offset(xOffset, yOffset);
298 | }
299 |
300 | _drawParagraphInsteadOfChart(ui.Canvas canvas, ui.Size size, String text) {
301 | double fontSize = 14.0;
302 | ui.ParagraphBuilder builder = new ui.ParagraphBuilder(
303 | new ui.ParagraphStyle(
304 | fontSize: fontSize,
305 | textAlign: TextAlign.center,
306 | ),
307 | )
308 | ..pushStyle(new ui.TextStyle(color: Colors.black))
309 | ..addText(text);
310 | final ui.Paragraph paragraph = builder.build()
311 | ..layout(new ui.ParagraphConstraints(width: size.width));
312 |
313 | canvas.drawParagraph(
314 | paragraph, new Offset(0.0, size.height / 2 - fontSize));
315 | }
316 | }
317 |
--------------------------------------------------------------------------------
/lib/widgets/progress_chart_dropdown.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class RangeOption {
4 | final int days;
5 | final String text;
6 |
7 | RangeOption(this.days, this.text);
8 | }
9 |
10 | class ProgressChartDropdown extends StatelessWidget {
11 | final int daysToShow;
12 | final Function(DateTime) onStartSelected;
13 |
14 | final List rangeOptions = [
15 | RangeOption(31, "month"),
16 | RangeOption(91, "3 months"),
17 | RangeOption(182, "6 months"),
18 | RangeOption(365, "year"),
19 | ];
20 |
21 | ProgressChartDropdown({Key key, this.daysToShow, this.onStartSelected})
22 | : super(key: key);
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return Row(
27 | crossAxisAlignment: CrossAxisAlignment.end,
28 | mainAxisAlignment: MainAxisAlignment.center,
29 | children: [
30 | _buildLabel(),
31 | _buildDropdown(_getSelectedOption()),
32 | ],
33 | );
34 | }
35 |
36 | RangeOption _getSelectedOption() {
37 | return rangeOptions.singleWhere(
38 | (option) => option.days == daysToShow,
39 | orElse: () => null,
40 | );
41 | }
42 |
43 | DropdownButton _buildDropdown(RangeOption selectedOption) {
44 | return DropdownButton(
45 | hint: Text("$daysToShow days"),
46 | value: selectedOption,
47 | items: rangeOptions.map(_optionToDropdownItem).toList(),
48 | onChanged: (option) {
49 | onStartSelected(
50 | DateTime.now().subtract(Duration(days: option.days - 1)));
51 | },
52 | );
53 | }
54 |
55 | DropdownMenuItem _optionToDropdownItem(option) {
56 | return DropdownMenuItem(
57 | child: Text(option.text),
58 | value: option,
59 | );
60 | }
61 |
62 | Padding _buildLabel() {
63 | return Padding(
64 | padding: const EdgeInsets.only(right: 8.0, bottom: 15.0),
65 | child: Text(
66 | "Show entries from last",
67 | style: TextStyle(color: Colors.grey[500]),
68 | ),
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/widgets/progress_chart_utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:weight_tracker/model/weight_entry.dart';
2 |
3 | /// Removes entries that are before beginningDate
4 | /// Adds an entry at the beginning of list if there is one before and after beginningDate
5 | List prepareEntryList(
6 | List initialEntries, DateTime beginningDate) {
7 | List entries = initialEntries
8 | .where((entry) =>
9 | entry.dateTime.isAfter(beginningDate) ||
10 | copyDateWithoutTime(entry.dateTime)
11 | .isAtSameMomentAs(copyDateWithoutTime(beginningDate)))
12 | .toList();
13 | if (entries.isNotEmpty &&
14 | _isMissingEntryFromBeginningDate(beginningDate, entries) &&
15 | _isAnyEntryBeforeBeginningDate(beginningDate, initialEntries)) {
16 | _addFakeEntryOnTheChartBeginning(initialEntries, entries, beginningDate);
17 | }
18 | return entries;
19 | }
20 |
21 | DateTime getStartDateOfChart(DateTime now, int daysToShow) {
22 | DateTime beginningOfChart = now.subtract(
23 | new Duration(days: daysToShow - 1, hours: now.hour, minutes: now.minute));
24 | return beginningOfChart;
25 | }
26 |
27 | DateTime copyDateWithoutTime(DateTime dateTime) {
28 | return new DateTime.utc(dateTime.year, dateTime.month, dateTime.day);
29 | }
30 |
31 | /// Adds missing entry at the start of a chart.
32 | ///
33 | /// If user has not put entry on the date which is first date of a chart,
34 | /// it takes last known weight before that date and estimates linearly weight on the beginning date.
35 | /// Then it creates and adds fake [WeightEntry] with that weight and date.
36 | void _addFakeEntryOnTheChartBeginning(List initialEntries,
37 | List entries, DateTime beginningDate) {
38 | List entriesNotInChart =
39 | initialEntries.where((entry) => !entries.contains(entry)).toList();
40 | WeightEntry firstEntryAfterBeginning = entries.last;
41 | WeightEntry lastEntryBeforeBeginning = entriesNotInChart.first;
42 | WeightEntry fakeEntry = new WeightEntry(
43 | beginningDate,
44 | _calculateWeightOnBeginningDate(
45 | lastEntryBeforeBeginning, firstEntryAfterBeginning, beginningDate),
46 | null);
47 | entries.add(fakeEntry);
48 | }
49 |
50 | bool _isMissingEntryFromBeginningDate(
51 | DateTime beginningDate, List entries) {
52 | return !entries.any((entry) =>
53 | entry.dateTime.day == beginningDate.day &&
54 | entry.dateTime.month == beginningDate.month &&
55 | entry.dateTime.year == beginningDate.year);
56 | }
57 |
58 | bool _isAnyEntryBeforeBeginningDate(
59 | DateTime beginningDate, List entries) {
60 | return entries.any((entry) => entry.dateTime.isBefore(beginningDate));
61 | }
62 |
63 | double _calculateWeightOnBeginningDate(WeightEntry lastEntryBeforeBeginning,
64 | WeightEntry firstEntryAfterBeginning, DateTime beginningDate) {
65 | DateTime firstEntryDateTime =
66 | copyDateWithoutTime(firstEntryAfterBeginning.dateTime);
67 | DateTime lastEntryDateTime =
68 | copyDateWithoutTime(lastEntryBeforeBeginning.dateTime);
69 |
70 | int differenceInDays =
71 | firstEntryDateTime.difference(lastEntryDateTime).inDays;
72 | double differenceInWeight =
73 | firstEntryAfterBeginning.weight - lastEntryBeforeBeginning.weight;
74 | int differenceInDaysFromBeginning =
75 | beginningDate.difference(lastEntryDateTime).inDays;
76 | double weightChangeFromLastEntry =
77 | (differenceInWeight * differenceInDaysFromBeginning) / differenceInDays;
78 | double estimatedWeight =
79 | lastEntryBeforeBeginning.weight + weightChangeFromLastEntry;
80 | return estimatedWeight;
81 | }
82 |
--------------------------------------------------------------------------------
/lib/widgets/weight_list_item.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:intl/intl.dart';
3 | import 'package:weight_tracker/logic/constants.dart';
4 | import 'package:weight_tracker/model/weight_entry.dart';
5 |
6 | class WeightListItem extends StatelessWidget {
7 | final WeightEntry weightEntry;
8 | final double weightDifference;
9 | final String unit;
10 |
11 | WeightListItem(this.weightEntry, this.weightDifference, this.unit);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | double displayWeight =
16 | unit == "kg" ? weightEntry.weight : weightEntry.weight * KG_LBS_RATIO;
17 | double displayDifference =
18 | unit == "kg" ? weightDifference : weightDifference * KG_LBS_RATIO;
19 | return new Padding(
20 | padding: new EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
21 | child: new Row(
22 | crossAxisAlignment: CrossAxisAlignment.center,
23 | mainAxisAlignment: MainAxisAlignment.end,
24 | children: [
25 | new Expanded(
26 | child: new Row(
27 | crossAxisAlignment: CrossAxisAlignment.start,
28 | children: [
29 | new Column(
30 | children: [
31 | new Text(
32 | new DateFormat.MMMEd().format(weightEntry.dateTime),
33 | textScaleFactor: 0.9,
34 | textAlign: TextAlign.left,
35 | ),
36 | new Text(
37 | new TimeOfDay.fromDateTime(weightEntry.dateTime)
38 | .format(context),
39 | textScaleFactor: 0.8,
40 | textAlign: TextAlign.right,
41 | style: new TextStyle(
42 | color: Colors.grey,
43 | ),
44 | ),
45 | ],
46 | crossAxisAlignment: CrossAxisAlignment.start,
47 | mainAxisSize: MainAxisSize.min,
48 | ),
49 | (weightEntry.note == null || weightEntry.note.isEmpty)
50 | ? new Container(
51 | height: 0.0,
52 | )
53 | : new Padding(
54 | padding: new EdgeInsets.only(left: 4.0),
55 | child: new Icon(
56 | Icons.speaker_notes,
57 | color: Colors.grey[300],
58 | size: 16.0,
59 | ),
60 | ),
61 | ],
62 | ),
63 | ),
64 | new Text(
65 | displayWeight.toStringAsFixed(1),
66 | textScaleFactor: 2.0,
67 | textAlign: TextAlign.center,
68 | ),
69 | new Expanded(
70 | child: new Row(
71 | mainAxisAlignment: MainAxisAlignment.end,
72 | children: [
73 | new Text(
74 | _differenceText(displayDifference),
75 | textScaleFactor: 1.6,
76 | textAlign: TextAlign.right,
77 | ),
78 | ],
79 | ),
80 | ),
81 | ],
82 | ),
83 | );
84 | }
85 |
86 | String _differenceText(double weightDifference) {
87 | if (weightDifference > 0) {
88 | return "+" + weightDifference.toStringAsFixed(1);
89 | } else if (weightDifference < 0) {
90 | return weightDifference.toStringAsFixed(1);
91 | } else {
92 | return "-";
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: weight_tracker
2 | description: A new flutter project.
3 |
4 | dependencies:
5 | flutter:
6 | sdk: flutter
7 | numberpicker: ^0.1.6
8 | firebase_auth: ^0.6.6
9 | firebase_database: ^1.0.5
10 | firebase_analytics: ^1.0.6
11 | redux: ^3.0.0
12 | flutter_redux: ^0.5.2
13 | tuple: ^1.0.2
14 | intl: ^0.15.7
15 | shared_preferences: ^0.4.3
16 | mockito: ^4.0.0
17 | google_sign_in: ^3.2.4
18 |
19 | dev_dependencies:
20 | flutter_test:
21 | sdk: flutter
22 |
23 |
24 | flutter:
25 |
26 | uses-material-design: true
27 | assets:
28 | - assets/scale-bathroom.png
29 | - assets/user.png
30 | - assets/google.png
31 |
--------------------------------------------------------------------------------
/test/unit_tests/chart_painter_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:mockito/mockito.dart';
4 | import 'package:test_api/test_api.dart';
5 | import 'package:weight_tracker/model/weight_entry.dart';
6 | import 'package:weight_tracker/widgets/progress_chart.dart';
7 |
8 | class MockCanvas extends Mock implements Canvas {}
9 |
10 | void main() {
11 | test("Given empty list ChartPainter draws only Paragraph", () {
12 | //given
13 | MockCanvas mockCanvas = new MockCanvas();
14 | Size size = new Size(600.0, 600.0);
15 | ChartPainter chartPainter = new ChartPainter([], 30, true);
16 | //when
17 | chartPainter.paint(mockCanvas, size);
18 | //then
19 | verifyNever(mockCanvas.drawCircle(any, any, any));
20 | verifyNever(mockCanvas.drawLine(any, any, any));
21 | verify(mockCanvas.drawParagraph(any, any)).called(1);
22 | });
23 |
24 | ///There are 5 horizontal lines
25 | test("Given one value, ChartPainter draws 5 lines and 1 point", () {
26 | //given
27 | MockCanvas mockCanvas = new MockCanvas();
28 | Size size = new Size(600.0, 600.0);
29 | WeightEntry weightEntry = new WeightEntry(new DateTime.now(), 70.0, null);
30 | ChartPainter chartPainter = new ChartPainter([weightEntry], 30, true);
31 | //when
32 | chartPainter.paint(mockCanvas, size);
33 | //then
34 | verify(mockCanvas.drawCircle(any, any, any)).called(1);
35 | verify(mockCanvas.drawLine(any, any, any)).called(5);
36 | });
37 |
38 | test("Given two values, ChartPainter draws 6 lines and 2 points", () {
39 | //given
40 | MockCanvas mockCanvas = new MockCanvas();
41 | Size size = new Size(600.0, 600.0);
42 | DateTime now = new DateTime.now();
43 | WeightEntry weightEntry1 = new WeightEntry(now, 70.0, null);
44 | WeightEntry weightEntry2 =
45 | new WeightEntry(now.subtract(const Duration(days: 1)), 70.0, null);
46 | ChartPainter chartPainter =
47 | new ChartPainter([weightEntry1, weightEntry2], 30, true);
48 | //when
49 | chartPainter.paint(mockCanvas, size);
50 | //then
51 | verify(mockCanvas.drawCircle(any, any, any)).called(2);
52 | verify(mockCanvas.drawLine(any, any, any)).called(6);
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/test/unit_tests/middleware_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_database/firebase_database.dart';
2 | import 'package:mockito/mockito.dart';
3 | import 'package:redux/redux.dart';
4 | import 'package:test_api/test_api.dart';
5 | import 'package:weight_tracker/logic/actions.dart';
6 | import 'package:weight_tracker/logic/middleware.dart';
7 | import 'package:weight_tracker/logic/redux_state.dart';
8 | import 'package:weight_tracker/model/weight_entry.dart';
9 |
10 | class StoreMock extends Mock implements Store {}
11 |
12 | class DatabaseReferenceMock extends Mock implements DatabaseReference {}
13 |
14 | ReduxState reducerMock(ReduxState state, action) {
15 | return state;
16 | }
17 |
18 | void main() {
19 | test('middleware AddEntryAction invokes push and set', () {
20 | //given
21 | DatabaseReferenceMock firebaseMock = new DatabaseReferenceMock();
22 | when(firebaseMock.push()).thenReturn(firebaseMock);
23 | ReduxState state = new ReduxState(
24 | firebaseState: new FirebaseState(mainReference: firebaseMock));
25 |
26 | Store store = new Store(reducerMock,
27 | initialState: state, middleware: [middleware].toList());
28 |
29 | WeightEntry weightEntry = new WeightEntry(new DateTime.now(), 70.0, null);
30 | AddEntryAction action = new AddEntryAction(weightEntry);
31 | //when
32 | store.dispatch(action);
33 | //then
34 | verify(firebaseMock.push()).called(1);
35 | verify(firebaseMock.set(weightEntry.toJson())).called(1);
36 | });
37 |
38 | test('middleware EditEntryAction invokes child and set', () {
39 | //given
40 | DatabaseReferenceMock firebaseMock = new DatabaseReferenceMock();
41 | when(firebaseMock.child(typed(any))).thenReturn(firebaseMock);
42 | ReduxState state = new ReduxState(
43 | firebaseState: new FirebaseState(mainReference: firebaseMock));
44 |
45 | Store store = new Store(reducerMock,
46 | initialState: state, middleware: [middleware].toList());
47 |
48 | WeightEntry weightEntry = new WeightEntry(new DateTime.now(), 70.0, null)
49 | ..key = "key";
50 | EditEntryAction action = new EditEntryAction(weightEntry);
51 | //when
52 | store.dispatch(action);
53 | //then
54 | verify(firebaseMock.child(weightEntry.key)).called(1);
55 | verify(firebaseMock.set(weightEntry.toJson())).called(1);
56 | });
57 |
58 | test('middleware RemoveEntryAction invokes child and remove', () {
59 | //given
60 | DatabaseReferenceMock firebaseMock = new DatabaseReferenceMock();
61 | when(firebaseMock.child(typed(any))).thenReturn(firebaseMock);
62 | ReduxState state = new ReduxState(
63 | firebaseState: new FirebaseState(mainReference: firebaseMock));
64 |
65 | Store store = new Store(reducerMock,
66 | initialState: state, middleware: [middleware].toList());
67 |
68 | WeightEntry weightEntry = new WeightEntry(new DateTime.now(), 70.0, null)
69 | ..key = "key";
70 | RemoveEntryAction action = new RemoveEntryAction(weightEntry);
71 | //when
72 | store.dispatch(action);
73 | //then
74 | verify(firebaseMock.child(weightEntry.key)).called(1);
75 | verify(firebaseMock.remove()).called(1);
76 | });
77 |
78 | test('middleware UndoRemovalAction invokes child and add', () {
79 | //given
80 | WeightEntry weightEntry = new WeightEntry(new DateTime.now(), 70.0, null)
81 | ..key = "key";
82 | DatabaseReferenceMock firebaseMock = new DatabaseReferenceMock();
83 | when(firebaseMock.child(weightEntry.key)).thenReturn(firebaseMock);
84 | ReduxState state = new ReduxState(
85 | firebaseState: new FirebaseState(mainReference: firebaseMock),
86 | removedEntryState: new RemovedEntryState(lastRemovedEntry: weightEntry),
87 | );
88 |
89 | Store store = new Store(reducerMock,
90 | initialState: state, middleware: [middleware].toList());
91 |
92 | UndoRemovalAction action = new UndoRemovalAction();
93 | //when
94 | store.dispatch(action);
95 | //then
96 | verify(firebaseMock.child(weightEntry.key)).called(1);
97 | verify(firebaseMock.set(weightEntry.toJson())).called(1);
98 | });
99 |
100 | test("Added database calls add entry when weight is saved", () {
101 | //given
102 | bool wasAddEntryCalled = false;
103 | var reducer = (ReduxState state, action) {
104 | if (action is AddEntryAction) {
105 | wasAddEntryCalled = true;
106 | }
107 | return state;
108 | };
109 | DatabaseReferenceMock databaseReferenceMock = new DatabaseReferenceMock();
110 | when(databaseReferenceMock.child(typed(any))).thenReturn(
111 | databaseReferenceMock);
112 | when(databaseReferenceMock.push()).thenReturn(databaseReferenceMock);
113 | ReduxState state = new ReduxState(
114 | weightFromNotes: 70.0,
115 | firebaseState: new FirebaseState(mainReference: databaseReferenceMock),
116 | );
117 | Store store = new Store(reducer,
118 | initialState: state, middleware: [middleware].toList());
119 | //when
120 | store.dispatch(new AddDatabaseReferenceAction(databaseReferenceMock));
121 | //then
122 | expect(wasAddEntryCalled, true);
123 | });
124 |
125 | test("Added database calls consume saved weight when weight is saved", () {
126 | //given
127 | bool wasConsumeSavedWeightCalled = false;
128 | var reducer = (ReduxState state, action) {
129 | if (action is ConsumeWeightFromNotes) {
130 | wasConsumeSavedWeightCalled = true;
131 | }
132 | return state;
133 | };
134 | DatabaseReferenceMock databaseReferenceMock = new DatabaseReferenceMock();
135 | when(databaseReferenceMock.child(typed(any))).thenReturn(
136 | databaseReferenceMock);
137 | when(databaseReferenceMock.push()).thenReturn(databaseReferenceMock);
138 | ReduxState state = new ReduxState(
139 | weightFromNotes: 70.0,
140 | firebaseState: new FirebaseState(mainReference: databaseReferenceMock),
141 | );
142 | Store store = new Store(reducer,
143 | initialState: state, middleware: [middleware].toList());
144 | //when
145 | store.dispatch(new AddDatabaseReferenceAction(databaseReferenceMock));
146 | //then
147 | expect(wasConsumeSavedWeightCalled, true);
148 | });
149 |
150 | test("Added database doesnt call consume/add when saved weight is null", () {
151 | //given
152 | bool wasConsumeOrAddCalled = false;
153 | var reducer = (ReduxState state, action) {
154 | if (action is ConsumeWeightFromNotes || action is AddEntryAction) {
155 | wasConsumeOrAddCalled = true;
156 | }
157 | return state;
158 | };
159 | DatabaseReferenceMock databaseReferenceMock = new DatabaseReferenceMock();
160 | ReduxState state = new ReduxState(
161 | weightFromNotes: null,
162 | );
163 | Store store = new Store(reducer,
164 | initialState: state, middleware: [middleware].toList());
165 | //when
166 | store.dispatch(new AddDatabaseReferenceAction(databaseReferenceMock));
167 | //then
168 | expect(wasConsumeOrAddCalled, false);
169 | });
170 | }
171 |
--------------------------------------------------------------------------------
/test/unit_tests/progress_chart_utils_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:test_api/test_api.dart';
2 | import 'package:weight_tracker/model/weight_entry.dart';
3 | import 'package:weight_tracker/widgets/progress_chart_utils.dart' as utils;
4 |
5 | void main() {
6 | test('general filtring list test', () {
7 | //given
8 | DateTime now = new DateTime.utc(2017, 1, 1, 8, 0);
9 | WeightEntry entry1 = new WeightEntry(now, 70.0, null);
10 | WeightEntry entry2 =
11 | new WeightEntry(now.subtract(new Duration(days: 6)), 70.0, null);
12 | WeightEntry entry3 =
13 | new WeightEntry(now.subtract(new Duration(days: 7)), 70.0, null);
14 | WeightEntry entry4 =
15 | new WeightEntry(now.subtract(new Duration(days: 8)), 70.0, null);
16 | int daysToShow = 7;
17 | List entries = [entry1, entry2, entry3, entry4];
18 | //when
19 | List newEntries =
20 | utils.prepareEntryList(entries, now.subtract(Duration(days: daysToShow-1)));
21 | //then
22 | expect(newEntries, contains(entry1));
23 | expect(newEntries, contains(entry2));
24 | expect(newEntries, isNot(contains(entry3)));
25 | expect(newEntries, isNot(contains(entry4)));
26 | });
27 |
28 | test('adds fake weight entry', () {
29 | //given
30 | int daysToShow = 2;
31 | DateTime now = new DateTime.utc(2017, 10, 10, 8, 0);
32 | WeightEntry firstEntryAfterBorder = new WeightEntry(now, 70.0, null);
33 | WeightEntry lastEntryBeforeBorder =
34 | new WeightEntry(now.subtract(new Duration(days: 2)), 90.0, null);
35 | List entries = [firstEntryAfterBorder, lastEntryBeforeBorder];
36 | //when
37 | List newEntries =
38 | utils.prepareEntryList(entries, now.subtract(Duration(days: daysToShow-1)));
39 | //then
40 | expect(newEntries, contains(firstEntryAfterBorder));
41 | expect(newEntries, isNot(contains(lastEntryBeforeBorder)));
42 | expect(
43 | newEntries,
44 | anyElement((WeightEntry entry) =>
45 | entry.weight == 80.0 && entry.dateTime.day == now.day - 1));
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/test/unit_tests/reducer_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:firebase_database/firebase_database.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:test_api/test_api.dart';
5 | import 'package:weight_tracker/logic/actions.dart';
6 | import 'package:weight_tracker/logic/reducer.dart';
7 | import 'package:weight_tracker/logic/redux_state.dart';
8 | import 'package:weight_tracker/model/weight_entry.dart';
9 |
10 | class FirebaseUserMock extends Mock implements FirebaseUser {}
11 |
12 | class DatabaseReferenceMock extends Mock implements DatabaseReference {}
13 |
14 | class EventMock extends Mock implements Event {}
15 |
16 | class DataSnapshotMock extends Mock implements DataSnapshot {
17 | Map _data;
18 |
19 | DataSnapshotMock(WeightEntry weightEntry) {
20 | _data = {
21 | "key": weightEntry.key,
22 | "value": {
23 | "weight": weightEntry.weight,
24 | "date": weightEntry.dateTime.millisecondsSinceEpoch,
25 | "note": weightEntry.note
26 | }
27 | };
28 | }
29 |
30 | String get key => _data['key'];
31 |
32 | dynamic get value => _data['value'];
33 | }
34 |
35 | void main() {
36 | test('reducer UserLoadedAction sets firebase user', () {
37 | //given
38 | ReduxState initialState = new ReduxState();
39 | FirebaseUser user = new FirebaseUserMock();
40 | UserLoadedAction action = new UserLoadedAction(user);
41 | //when
42 | ReduxState newState = reduce(initialState, action);
43 | //then
44 | expect(newState.firebaseState.firebaseUser, user);
45 | });
46 |
47 | test('reducer AddDatabaseReferenceAction sets database reference', () {
48 | //given
49 | ReduxState initialState = new ReduxState();
50 | DatabaseReference databaseReference = new DatabaseReferenceMock();
51 | AddDatabaseReferenceAction action =
52 | new AddDatabaseReferenceAction(databaseReference);
53 | //when
54 | ReduxState newState = reduce(initialState, action);
55 | //then
56 | expect(newState.firebaseState.mainReference, databaseReference);
57 | });
58 |
59 | test('reducer AcceptEntryAddedAction sets flag to false', () {
60 | //given
61 | ReduxState initialState = new ReduxState(
62 | mainPageState: new MainPageReduxState(hasEntryBeenAdded: true));
63 | AcceptEntryAddedAction action = new AcceptEntryAddedAction();
64 | //when
65 | ReduxState newState = reduce(initialState, action);
66 | //then
67 | expect(newState.mainPageState.hasEntryBeenAdded, false);
68 | });
69 |
70 | test('reducer AcceptEntryAddedAction flag false stays false', () {
71 | //given
72 | ReduxState initialState = new ReduxState();
73 | AcceptEntryAddedAction action = new AcceptEntryAddedAction();
74 | //when
75 | ReduxState newState = reduce(initialState, action);
76 | //then
77 | expect(newState.mainPageState.hasEntryBeenAdded, false);
78 | });
79 |
80 | test('reducer AcceptEntryRemovalAction sets flag to false', () {
81 | //given
82 | ReduxState initialState = new ReduxState(
83 | removedEntryState: new RemovedEntryState(hasEntryBeenRemoved: true));
84 | expect(initialState.removedEntryState.hasEntryBeenRemoved, true);
85 | AcceptEntryRemovalAction action = new AcceptEntryRemovalAction();
86 | //when
87 | ReduxState newState = reduce(initialState, action);
88 | //then
89 | expect(newState.removedEntryState.hasEntryBeenRemoved, false);
90 | });
91 |
92 | test('reducer AcceptEntryRemovalAction flag false stays false', () {
93 | //given
94 | ReduxState initialState = new ReduxState();
95 | AcceptEntryRemovalAction action = new AcceptEntryRemovalAction();
96 | //when
97 | ReduxState newState = reduce(initialState, action);
98 | //then
99 | expect(newState.removedEntryState.hasEntryBeenRemoved, false);
100 | });
101 |
102 | test('reducer OnUnitChangedAction changes unit', () {
103 | //given
104 | ReduxState initialState = new ReduxState(unit: 'initialUnit');
105 | OnUnitChangedAction action = new OnUnitChangedAction("newUnit");
106 | //when
107 | ReduxState newState = reduce(initialState, action);
108 | //then
109 | expect(newState.unit, 'newUnit');
110 | });
111 |
112 | test('reducer UpdateActiveWeightEntry changes entry', () {
113 | //given
114 | ReduxState initialState = new ReduxState();
115 | WeightEntry updatedEntry =
116 | new WeightEntry(new DateTime.now(), 60.0, "text");
117 | UpdateActiveWeightEntry action = new UpdateActiveWeightEntry(updatedEntry);
118 | //when
119 | ReduxState newState = reduce(initialState, action);
120 | //then
121 | expect(newState.weightEntryDialogState.activeEntry, updatedEntry);
122 | });
123 |
124 | test('reducer OpenEditEntryDialog changes entry', () {
125 | //given
126 | ReduxState initialState = new ReduxState();
127 | WeightEntry updatedEntry =
128 | new WeightEntry(new DateTime.now(), 60.0, "text");
129 | OpenEditEntryDialog action = new OpenEditEntryDialog(updatedEntry);
130 | //when
131 | ReduxState newState = reduce(initialState, action);
132 | //then
133 | expect(newState.weightEntryDialogState.activeEntry, updatedEntry);
134 | });
135 |
136 | test('reducer OpenEditEntryDialog sets EditMode to true', () {
137 | //given
138 | ReduxState initialState = new ReduxState();
139 | WeightEntry updatedEntry =
140 | new WeightEntry(new DateTime.now(), 60.0, "text");
141 | OpenEditEntryDialog action = new OpenEditEntryDialog(updatedEntry);
142 | //when
143 | ReduxState newState = reduce(initialState, action);
144 | //then
145 | expect(newState.weightEntryDialogState.isEditMode, true);
146 | });
147 |
148 | test('reducer OpenAddEntryDialog sets EditMode to false', () {
149 | //given
150 | ReduxState initialState = new ReduxState(
151 | weightEntryDialogState:
152 | new WeightEntryDialogReduxState(isEditMode: true));
153 | OpenAddEntryDialog action = new OpenAddEntryDialog();
154 | //when
155 | ReduxState newState = reduce(initialState, action);
156 | //then
157 | expect(newState.weightEntryDialogState.isEditMode, false);
158 | });
159 |
160 | test('reducer OpenAddEntryDialog creates new entry with weight 70', () {
161 | //given
162 | ReduxState initialState = new ReduxState();
163 | OpenAddEntryDialog action = new OpenAddEntryDialog();
164 | //when
165 | ReduxState newState = reduce(initialState, action);
166 | //then
167 | expect(newState.weightEntryDialogState.activeEntry?.weight, 70);
168 | });
169 |
170 | test(
171 | 'reducer OpenAddEntryDialog creates new entry with copied weight from first entry',
172 | () {
173 | //given
174 | ReduxState initialState = new ReduxState(
175 | entries: [new WeightEntry(new DateTime.now(), 60.0, "Text")]);
176 | OpenAddEntryDialog action = new OpenAddEntryDialog();
177 | //when
178 | ReduxState newState = reduce(initialState, action);
179 | //then
180 | expect(newState.weightEntryDialogState.activeEntry?.weight, 60);
181 | expect(newState.weightEntryDialogState.activeEntry?.note, null);
182 | });
183 |
184 | test('reducer OnAddedAction adds entry to list', () {
185 | //given
186 | WeightEntry entry = createEntry("key", new DateTime.now(), 60.0, null);
187 | ReduxState initialState = new ReduxState();
188 | OnAddedAction action = new OnAddedAction(createEventMock(entry));
189 | //when
190 | ReduxState newState = reduce(initialState, action);
191 | //then
192 | expect(newState.entries, contains(entry));
193 | });
194 |
195 | test('reducer OnAddedAction sets hasEntryBeenAdded to true', () {
196 | //given
197 | WeightEntry entry = createEntry("key", new DateTime.now(), 60.0, null);
198 | ReduxState initialState = new ReduxState();
199 | OnAddedAction action = new OnAddedAction(createEventMock(entry));
200 | //when
201 | ReduxState newState = reduce(initialState, action);
202 | //then
203 | expect(newState.mainPageState.hasEntryBeenAdded, true);
204 | });
205 |
206 | test('reducer OnRemovedAction sets hasEntryBeenRemoved to true', () {
207 | //given
208 | WeightEntry entry = createEntry("key", new DateTime.now(), 60.0, null);
209 | ReduxState initialState = new ReduxState(entries: [entry]);
210 | OnRemovedAction action = new OnRemovedAction(createEventMock(entry));
211 | //when
212 | ReduxState newState = reduce(initialState, action);
213 | //then
214 | expect(newState.removedEntryState.hasEntryBeenRemoved, true);
215 | });
216 |
217 | test('reducer OnRemovedAction removes entry from list', () {
218 | //given
219 | WeightEntry entry = createEntry("key", new DateTime.now(), 60.0, null);
220 | ReduxState initialState = new ReduxState(entries: [entry]);
221 | OnRemovedAction action = new OnRemovedAction(createEventMock(entry));
222 | //when
223 | ReduxState newState = reduce(initialState, action);
224 | //then
225 | expect(newState.entries, isEmpty);
226 | });
227 |
228 | test('reducer OnRemovedAction sets lastRemovedEntry', () {
229 | //given
230 | WeightEntry entry = createEntry("key", new DateTime.now(), 60.0, null);
231 | ReduxState initialState = new ReduxState(entries: [entry]);
232 | OnRemovedAction action = new OnRemovedAction(createEventMock(entry));
233 | //when
234 | ReduxState newState = reduce(initialState, action);
235 | //then
236 | expect(newState.removedEntryState.lastRemovedEntry, entry);
237 | });
238 |
239 | }
240 |
241 | WeightEntry createEntry(String key, DateTime dateTime, double weight,
242 | String note) {
243 | WeightEntry entry = new WeightEntry(dateTime, weight, note);
244 | entry.key = key;
245 | return entry;
246 | }
247 |
248 | Event createEventMock(WeightEntry weightEntry) {
249 | EventMock eventMock = new EventMock();
250 | DataSnapshotMock snapshotMock = new DataSnapshotMock(weightEntry);
251 | when(eventMock.snapshot).thenReturn(snapshotMock);
252 | return eventMock;
253 | }
254 |
--------------------------------------------------------------------------------
/test/widget_tests/chart_dropdown_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:weight_tracker/widgets/progress_chart_dropdown.dart';
4 |
5 | void main() {
6 | testWidgets('If 10 days to show, then 10 days label is displayed',
7 | (WidgetTester tester) async {
8 | await _pumpDropdown(tester, daysToShow: 10);
9 | expect(find.text("10 days"), findsOneWidget);
10 | });
11 |
12 | testWidgets('If 31 days to show, then 1 month label is displayed',
13 | (WidgetTester tester) async {
14 | await _pumpDropdown(tester, daysToShow: 31);
15 | expect(find.text("month"), findsOneWidget);
16 | });
17 |
18 | testWidgets('If 91 days to show, then 3 months label is displayed',
19 | (WidgetTester tester) async {
20 | await _pumpDropdown(tester, daysToShow: 91);
21 | expect(find.text("3 months"), findsOneWidget);
22 | });
23 |
24 | testWidgets('If 182 days to show, then 6 months label is displayed',
25 | (WidgetTester tester) async {
26 | await _pumpDropdown(tester, daysToShow: 182);
27 | expect(find.text("6 months"), findsOneWidget);
28 | });
29 |
30 | testWidgets('If 365 days to show, then 1 year label is displayed',
31 | (WidgetTester tester) async {
32 | await _pumpDropdown(tester, daysToShow: 365);
33 | expect(find.text("year"), findsOneWidget);
34 | });
35 | }
36 |
37 | _pumpDropdown(WidgetTester tester,
38 | {int daysToShow, Function(DateTime) onStartSelected}) async {
39 | return tester.pumpWidget(
40 | MaterialApp(
41 | home: Scaffold(
42 | body: ProgressChartDropdown(
43 | daysToShow: daysToShow,
44 | onStartSelected: onStartSelected,
45 | ),
46 | ),
47 | ),
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/test/widget_tests/history_page_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:redux/redux.dart';
5 | import 'package:weight_tracker/logic/reducer.dart';
6 | import 'package:weight_tracker/logic/redux_state.dart';
7 | import 'package:weight_tracker/model/weight_entry.dart';
8 | import 'package:weight_tracker/screens/history_page.dart';
9 | import 'package:weight_tracker/widgets/weight_list_item.dart';
10 |
11 | void main() {
12 | WeightEntry entry = new WeightEntry(new DateTime.now(), 70.0, null);
13 | ReduxState defaultState = new ReduxState(unit: 'kg', entries: [entry, entry]);
14 |
15 | pumpSettingWidget(Store store, WidgetTester tester) async {
16 | await tester.pumpWidget(new StatefulBuilder(
17 | builder: (BuildContext context, StateSetter setState) {
18 | return new StoreProvider(
19 | store: store,
20 | child: new MaterialApp(home: new Scaffold(body: new HistoryPage())),
21 | );
22 | }));
23 | }
24 |
25 | testWidgets('HistoryPage has text if there are no entries',
26 | (WidgetTester tester) async {
27 | await pumpSettingWidget(
28 | new Store(
29 | reduce, initialState: defaultState.copyWith(entries: [])),
30 | tester);
31 | expect(find.text('Add your weight to see history'), findsOneWidget);
32 | });
33 |
34 | testWidgets('HistoryPage has ListView', (WidgetTester tester) async {
35 | await pumpSettingWidget(
36 | new Store(reduce, initialState: defaultState), tester);
37 | expect(find.byType(ListView), findsOneWidget);
38 | });
39 |
40 | testWidgets('HistoryPage has 2 items for 2 entries',
41 | (WidgetTester tester) async {
42 | await pumpSettingWidget(
43 | new Store(reduce, initialState: defaultState), tester);
44 | expect(find.byType(WeightListItem), findsNWidgets(2));
45 | });
46 |
47 | testWidgets('HistoryPage shows snackbar if entry was removed',
48 | (WidgetTester tester) async {
49 | await pumpSettingWidget(
50 | new Store(
51 | reduce,
52 | initialState: defaultState.copyWith(
53 | removedEntryState: defaultState.removedEntryState
54 | .copyWith(hasEntryBeenRemoved: true),
55 | ),
56 | ),
57 | tester);
58 | await tester.pump(new Duration(milliseconds: 100));
59 | expect(find.byType(SnackBar), findsOneWidget);
60 | });
61 |
62 | testWidgets('HistoryPage shows snackbar with proper text',
63 | (WidgetTester tester) async {
64 | await pumpSettingWidget(
65 | new Store(
66 | reduce,
67 | initialState: defaultState.copyWith(
68 | removedEntryState: defaultState.removedEntryState
69 | .copyWith(hasEntryBeenRemoved: true),
70 | ),
71 | ),
72 | tester);
73 | await tester.pump(new Duration(milliseconds: 100));
74 | expect(find.text('Entry deleted.'), findsOneWidget);
75 | });
76 |
77 | testWidgets('HistoryPage shows snackbar with proper action',
78 | (WidgetTester tester) async {
79 | await pumpSettingWidget(
80 | new Store(
81 | reduce,
82 | initialState: defaultState.copyWith(
83 | removedEntryState: defaultState.removedEntryState
84 | .copyWith(hasEntryBeenRemoved: true),
85 | ),
86 | ),
87 | tester);
88 | await tester.pump(new Duration(milliseconds: 100));
89 | expect(find.widgetWithText(SnackBarAction, 'UNDO'), findsOneWidget);
90 | });
91 | }
92 |
--------------------------------------------------------------------------------
/test/widget_tests/main_page_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:redux/redux.dart';
5 | import 'package:weight_tracker/logic/actions.dart';
6 | import 'package:weight_tracker/logic/redux_state.dart';
7 | import 'package:weight_tracker/main.dart';
8 | import 'package:weight_tracker/screens/main_page.dart';
9 | import 'package:weight_tracker/screens/statistics_page.dart';
10 |
11 | void main() {
12 | testWidgets('App name in header', (WidgetTester tester) async {
13 | await tester.pumpWidget(new MyApp());
14 | expect(find.widgetWithText(AppBar, 'Weight Tracker'), findsOneWidget);
15 | });
16 |
17 | testWidgets('Main screen has two tabs', (WidgetTester tester) async {
18 | await tester.pumpWidget(new MyApp());
19 | expect(find.byType(Tab), findsNWidgets(2));
20 | });
21 |
22 | testWidgets(
23 | 'Main screen has statistics tab in bar', (WidgetTester tester) async {
24 | await tester.pumpWidget(new MyApp());
25 | expect(
26 | find.byWidgetPredicate((widget) =>
27 | widget is Tab &&
28 | widget.key == new Key('StatisticsTab') &&
29 | widget.text == 'STATISTICS' &&
30 | (widget.icon as Icon).icon == Icons.show_chart),
31 | findsOneWidget);
32 | });
33 |
34 | testWidgets('Main screen has statistics tab in tabview ', (
35 | WidgetTester tester) async {
36 | await tester.pumpWidget(new MyApp());
37 | expect(find.byType(StatisticsPage), findsOneWidget);
38 | });
39 |
40 | testWidgets(
41 | 'Main screen has history tab in bar', (WidgetTester tester) async {
42 | await tester.pumpWidget(new MyApp());
43 | expect(
44 | find.byWidgetPredicate((widget) =>
45 | widget is Tab &&
46 | widget.key == new Key('HistoryTab') &&
47 | widget.text == 'HISTORY' &&
48 | (widget.icon as Icon).icon == Icons.history),
49 | findsOneWidget);
50 | });
51 |
52 | testWidgets("Main screen calls GetSaveNote", (WidgetTester tester) async {
53 | bool wasGetSavedNoteCalled = false;
54 | var reduce = (ReduxState state, action) {
55 | if (action is GetSavedWeightNote) {
56 | wasGetSavedNoteCalled = true;
57 | }
58 | return state;
59 | };
60 | Store store = new Store(reduce, initialState: new ReduxState());
61 | await tester.pumpWidget(
62 | new StoreProvider(
63 | store: store,
64 | child: new MaterialApp(
65 | home: new MainPage(title: "Weight Tracker"),
66 | ),
67 | )
68 | );
69 | expect(wasGetSavedNoteCalled, true);
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/test/widget_tests/settings_page_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:redux/redux.dart';
5 | import 'package:weight_tracker/logic/middleware.dart';
6 | import 'package:weight_tracker/logic/reducer.dart';
7 | import 'package:weight_tracker/logic/redux_state.dart';
8 | import 'package:weight_tracker/screens/settings_screen.dart';
9 |
10 | void main() {
11 | final Store store = new Store(reduce,
12 | initialState: new ReduxState(),
13 | middleware: [middleware].toList());
14 |
15 | pumpSettingWidget(WidgetTester tester) async {
16 | await tester.pumpWidget(new StatefulBuilder(
17 | builder: (BuildContext context, StateSetter setState) {
18 | return new StoreProvider(
19 | store: store,
20 | child: new MaterialApp(home: new SettingsPage()),
21 | );
22 | }));
23 | }
24 |
25 | testWidgets('SettingsPage has "Settings" in header',
26 | (WidgetTester tester) async {
27 | await pumpSettingWidget(tester);
28 | expect(find.widgetWithText(AppBar, 'Settings'), findsOneWidget);
29 | });
30 |
31 | testWidgets('SettingsPage has Unit label', (WidgetTester tester) async {
32 | await pumpSettingWidget(tester);
33 | expect(find.text('Unit'), findsOneWidget);
34 | });
35 |
36 | testWidgets('Settings has spinner with kg and lbs',
37 | (WidgetTester tester) async {
38 | await pumpSettingWidget(tester);
39 | expect(find.byKey(const Key('UnitDropdown')), findsOneWidget);
40 | expect(find.text('kg'), findsOneWidget);
41 | expect(find.text('lbs'), findsOneWidget);
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/test/widget_tests/weight_entry_dialog_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_redux/flutter_redux.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:numberpicker/numberpicker.dart';
5 | import 'package:redux/redux.dart';
6 | import 'package:weight_tracker/logic/actions.dart';
7 | import 'package:weight_tracker/logic/reducer.dart';
8 | import 'package:weight_tracker/logic/redux_state.dart';
9 | import 'package:weight_tracker/model/weight_entry.dart';
10 | import 'package:weight_tracker/screens/weight_entry_dialog.dart';
11 | import 'package:matcher/matcher.dart' as matchers;
12 |
13 | void main() {
14 | WeightEntry activeEntry = new WeightEntry(new DateTime.now(), 70.0, null);
15 | WeightEntryDialogReduxState dialogState = new WeightEntryDialogReduxState(
16 | isEditMode: true, activeEntry: activeEntry);
17 | WeightEntryDialogReduxState dialogAddState =
18 | dialogState.copyWith(isEditMode: false);
19 | ReduxState defaultState = new ReduxState(weightEntryDialogState: dialogState);
20 |
21 | pumpSettingWidget(Store store, WidgetTester tester) async {
22 | await tester.pumpWidget(new StatefulBuilder(
23 | builder: (BuildContext context, StateSetter setState) {
24 | return new StoreProvider(
25 | store: store,
26 | child: new MaterialApp(home: new WeightEntryDialog()),
27 | );
28 | }));
29 | }
30 |
31 | testWidgets('WeightEntryDialog has "Edit entry" in header',
32 | (WidgetTester tester) async {
33 | await pumpSettingWidget(
34 | new Store(reduce, initialState: defaultState), tester);
35 | expect(find.widgetWithText(AppBar, 'Edit entry'), findsOneWidget);
36 | });
37 |
38 | testWidgets('WeightEntryDialog has "New entry" in header',
39 | (WidgetTester tester) async {
40 | await pumpSettingWidget(
41 | new Store(reduce,
42 | initialState:
43 | defaultState.copyWith(weightEntryDialogState: dialogAddState)),
44 | tester);
45 | expect(find.widgetWithText(AppBar, 'New entry'), findsOneWidget);
46 | });
47 |
48 | testWidgets('WeightEntryDialog has "SAVE" button when edit',
49 | (WidgetTester tester) async {
50 | await pumpSettingWidget(
51 | new Store(reduce, initialState: defaultState), tester);
52 | expect(find.widgetWithText(FlatButton, 'SAVE'), findsOneWidget);
53 | });
54 |
55 | testWidgets('WeightEntryDialog has "SAVE" button when not edit',
56 | (WidgetTester tester) async {
57 | await pumpSettingWidget(
58 | new Store(reduce,
59 | initialState:
60 | defaultState.copyWith(weightEntryDialogState: dialogAddState)),
61 | tester);
62 | expect(find.widgetWithText(FlatButton, 'SAVE'), findsOneWidget);
63 | });
64 |
65 | testWidgets('WeightEntryDialog has "DELETE" button when edit',
66 | (WidgetTester tester) async {
67 | await pumpSettingWidget(
68 | new Store(reduce, initialState: defaultState), tester);
69 | expect(find.widgetWithText(FlatButton, 'DELETE'), findsOneWidget);
70 | });
71 |
72 | testWidgets('WeightEntryDialog has not "DELETE" button when not edit',
73 | (WidgetTester tester) async {
74 | await pumpSettingWidget(
75 | new Store(reduce,
76 | initialState:
77 | defaultState.copyWith(weightEntryDialogState: dialogAddState)),
78 | tester);
79 | expect(find.widgetWithText(FlatButton, 'DELETE'), findsNothing);
80 | });
81 |
82 | testWidgets('WeightEntryDialog displays weight in kg',
83 | (WidgetTester tester) async {
84 | await pumpSettingWidget(
85 | new Store(reduce, initialState: defaultState), tester);
86 | expect(find.text('70.0 kg'), findsOneWidget);
87 | });
88 |
89 | testWidgets('WeightEntryDialog displays weight in lbs',
90 | (WidgetTester tester) async {
91 | await pumpSettingWidget(
92 | new Store(
93 | reduce, initialState: defaultState.copyWith(unit: 'lbs')),
94 | tester);
95 | expect(find.text('154.0 lbs'), findsOneWidget);
96 | });
97 |
98 | testWidgets('WeightEntryDialog displays hint when note is null',
99 | (WidgetTester tester) async {
100 | await pumpSettingWidget(
101 | new Store(reduce, initialState: defaultState), tester);
102 | expect(find.text('Optional note'), findsOneWidget);
103 | });
104 |
105 | //DatePickerDialog is private
106 | testWidgets('WeightEntryDialog opens MonthPicker on date click',
107 | (WidgetTester tester) async {
108 | await pumpSettingWidget(
109 | new Store(reduce, initialState: defaultState), tester);
110 | await tester.tap(find.byKey(new Key('CalendarItem')));
111 | await tester.pump();
112 | expect(find.byType(MonthPicker), findsOneWidget);
113 | });
114 |
115 | //TimePicker is private
116 | testWidgets('WeightEntryDialog opens Dialog on time click',
117 | (WidgetTester tester) async {
118 | await pumpSettingWidget(
119 | new Store(reduce, initialState: defaultState), tester);
120 | await tester.tap(find.byKey(new Key('TimeItem')));
121 | await tester.pump();
122 | expect(find.byType(Dialog), findsOneWidget);
123 | });
124 |
125 | testWidgets('WeightEntryDialog opens NumberPickerDialog on weight click',
126 | (WidgetTester tester) async {
127 | await pumpSettingWidget(
128 | new Store(reduce, initialState: defaultState), tester);
129 | await tester.tap(find.text('70.0 kg'));
130 | await tester.pump();
131 | expect(find.byType(NumberPickerDialog), findsOneWidget);
132 | expect(find.text('70'), findsOneWidget);
133 | expect(find.text('0'), findsOneWidget);
134 | });
135 |
136 | testWidgets('Clicking Save on edit invokes EditEntryAction with activeEntry',
137 | (WidgetTester tester) async {
138 | WeightEntry entry = new WeightEntry(new DateTime.now(), 70.0, null);
139 | var reducer = (state, action) {
140 | expect(action, matchers.TypeMatcher());
141 | expect((action as EditEntryAction).weightEntry, entry);
142 | };
143 | await pumpSettingWidget(
144 | new Store(
145 | reducer,
146 | initialState: defaultState.copyWith(
147 | weightEntryDialogState: dialogState.copyWith(
148 | activeEntry: entry),
149 | ),
150 | ),
151 | tester);
152 | await tester.tap(find.text('SAVE'));
153 | });
154 |
155 | testWidgets('Clicking Save on create invokes AddEntryAction with ActiveEntry',
156 | (WidgetTester tester) async {
157 | WeightEntry entry = new WeightEntry(new DateTime.now(), 70.0, null);
158 | var reducer = (state, action) {
159 | expect(action, matchers.TypeMatcher());
160 | expect((action as AddEntryAction).weightEntry, entry);
161 | };
162 | await pumpSettingWidget(
163 | new Store(
164 | reducer,
165 | initialState: defaultState.copyWith(
166 | weightEntryDialogState: dialogAddState.copyWith(
167 | activeEntry: entry),
168 | ),
169 | ),
170 | tester);
171 | await tester.tap(find.text('SAVE'));
172 | });
173 |
174 | testWidgets('Clicking Delete invokes RemoveEntryAction with activeEntry',
175 | (WidgetTester tester) async {
176 | WeightEntry entry = new WeightEntry(new DateTime.now(), 70.0, null);
177 | var reducer = (state, action) {
178 | expect(action, matchers.TypeMatcher());
179 | expect((action as RemoveEntryAction).weightEntry, entry);
180 | };
181 | await pumpSettingWidget(
182 | new Store(
183 | reducer,
184 | initialState: defaultState.copyWith(
185 | weightEntryDialogState: dialogState.copyWith(
186 | activeEntry: entry),
187 | ),
188 | ),
189 | tester);
190 | await tester.tap(find.text('DELETE'));
191 | });
192 |
193 | testWidgets('Changing note updates activeEntry', (WidgetTester tester) async {
194 | WeightEntry entry = new WeightEntry(new DateTime.now(), 70.0, null);
195 | Store store = new Store(reduce,
196 | initialState: defaultState.copyWith(
197 | weightEntryDialogState: dialogState.copyWith(activeEntry: entry),
198 | ));
199 | await pumpSettingWidget(store, tester);
200 | await tester.enterText(find.byType(TextField), 'Lorem');
201 | expect(store.state.weightEntryDialogState.activeEntry.note, 'Lorem');
202 | });
203 | }
204 |
--------------------------------------------------------------------------------
/test/widget_tests/weight_list_item_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:weight_tracker/model/weight_entry.dart';
4 | import 'package:weight_tracker/widgets/weight_list_item.dart';
5 |
6 | //materialapp and scaffold are needed for formatting date
7 | void main() {
8 | testWidgets('Displays weight and difference in kg',
9 | (WidgetTester tester) async {
10 | WeightEntry entry = new WeightEntry(new DateTime.now(), 60.0, null);
11 | await tester.pumpWidget(new MaterialApp(
12 | home: new Scaffold(body: new WeightListItem(entry, 10.0, 'kg'))));
13 |
14 | expect(find.text('60.0'), findsOneWidget);
15 | expect(find.text('+10.0'), findsOneWidget);
16 | });
17 |
18 | testWidgets('Displays weight and difference in lbs',
19 | (WidgetTester tester) async {
20 | WeightEntry entry = new WeightEntry(new DateTime.now(), 60.0, null);
21 | await tester.pumpWidget(new MaterialApp(
22 | home: new Scaffold(body: new WeightListItem(entry, 10.0, 'lbs'))));
23 |
24 | expect(find.text('132.0'), findsOneWidget);
25 | expect(find.text('+22.0'), findsOneWidget);
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/weight_tracker.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------