├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android.iml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── me │ │ │ └── danics │ │ │ └── firestoretest │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── 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 │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app-release.apk ├── assets ├── flare │ └── Like.flr ├── fonts │ └── Billabong.ttf └── images │ └── google_signin_button.png ├── firestore_test.iml ├── firestore_test_android.iml ├── functions ├── lib │ ├── getFeed.js │ ├── getFeed.js.map │ ├── index.js │ ├── index.js.map │ ├── notificationHandler.js │ └── notificationHandler.js.map ├── package-lock.json ├── package.json ├── src │ ├── getFeed.ts │ ├── index.ts │ └── notificationHandler.ts ├── tsconfig.json └── tslint.json ├── 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 └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── activity_feed.dart ├── comment_screen.dart ├── create_account.dart ├── edit_profile_page.dart ├── feed.dart ├── image_post.dart ├── location.dart ├── main.dart ├── models │ └── user.dart ├── profile_page.dart ├── search_page.dart └── upload_page.dart ├── pubspec.lock └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .vscode/ 5 | .packages 6 | .pub/ 7 | build/ 8 | ios/.generated/ 9 | packages 10 | .flutter-plugins 11 | functions/node_modules/ 12 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 3ea4d06340a97a1e9d7cae97567c64e0569dcaa2 8 | channel: beta 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [1.2.3] - 2020-01-04 5 | ### Changes 6 | - Add animation to Heart when liking a post 7 | - add user's post to their own feed 8 | - add logout button to edit profile page 9 | - tabs now keep state after switching pages, improves overall ux 10 | - Code clean up 11 | - Dependency bumps 12 | - Fixed bugs 13 | - remove width constraint on follow button 14 | - feed now loads after username selection 15 | - fix duplicate username screens on first google signin 16 | - fixed error on open + removed useless code 17 | - Set maxWidth and maxHeight on pickImage() 18 | - added missing keys 19 | 20 | 21 | ## [1.2.2] - 2018-11-07 22 | ### Changes 23 | - New - Location suggestions on upload page 24 | 25 | 26 | ## [1.2.1] - 2018-11-04 27 | ### Changes 28 | - Changed `BottomNavigationBar` to `CupertinoTabBar` to remove empty space underneath icons 29 | - Added highlighting for the active page on the `CupertinoTabBar` 30 | 31 | 32 | ## [1.2.0] - 2018-10-26 33 | ### Changes 34 | - Added push notifications with Cloud Functions (Android) 35 | - Get notifications when someone likes or comments on a post and follows you 36 | - Notifications are for android as iOS notifications need an Apple developer account. 37 | - Cloud Functions Changes 38 | - Separated functions to individual files 39 | - npm dependencies updated 40 | 41 | 42 | ## [1.1.0] - 2018-10-22 43 | ### Changes 44 | - Updated to most recent dependencies 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | Public License 3 | 4 | By exercising the Licensed Rights (defined below), You accept and agree 5 | to be bound by the terms and conditions of this Creative Commons 6 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 7 | ("Public License"). To the extent this Public License may be 8 | interpreted as a contract, You are granted the Licensed Rights in 9 | consideration of Your acceptance of these terms and conditions, and the 10 | Licensor grants You such rights in consideration of benefits the 11 | Licensor receives from making the Licensed Material available under 12 | these terms and conditions. 13 | 14 | 15 | Section 1 -- Definitions. 16 | 17 | a. Adapted Material means material subject to Copyright and Similar 18 | Rights that is derived from or based upon the Licensed Material 19 | and in which the Licensed Material is translated, altered, 20 | arranged, transformed, or otherwise modified in a manner requiring 21 | permission under the Copyright and Similar Rights held by the 22 | Licensor. For purposes of this Public License, where the Licensed 23 | Material is a musical work, performance, or sound recording, 24 | Adapted Material is always produced where the Licensed Material is 25 | synched in timed relation with a moving image. 26 | 27 | b. Adapter's License means the license You apply to Your Copyright 28 | and Similar Rights in Your contributions to Adapted Material in 29 | accordance with the terms and conditions of this Public License. 30 | 31 | c. BY-NC-SA Compatible License means a license listed at 32 | creativecommons.org/compatiblelicenses, approved by Creative 33 | Commons as essentially the equivalent of this Public License. 34 | 35 | d. Copyright and Similar Rights means copyright and/or similar rights 36 | closely related to copyright including, without limitation, 37 | performance, broadcast, sound recording, and Sui Generis Database 38 | Rights, without regard to how the rights are labeled or 39 | categorized. For purposes of this Public License, the rights 40 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 41 | Rights. 42 | 43 | e. Effective Technological Measures means those measures that, in the 44 | absence of proper authority, may not be circumvented under laws 45 | fulfilling obligations under Article 11 of the WIPO Copyright 46 | Treaty adopted on December 20, 1996, and/or similar international 47 | agreements. 48 | 49 | f. Exceptions and Limitations means fair use, fair dealing, and/or 50 | any other exception or limitation to Copyright and Similar Rights 51 | that applies to Your use of the Licensed Material. 52 | 53 | g. License Elements means the license attributes listed in the name 54 | of a Creative Commons Public License. The License Elements of this 55 | Public License are Attribution, NonCommercial, and ShareAlike. 56 | 57 | h. Licensed Material means the artistic or literary work, database, 58 | or other material to which the Licensor applied this Public 59 | License. 60 | 61 | i. Licensed Rights means the rights granted to You subject to the 62 | terms and conditions of this Public License, which are limited to 63 | all Copyright and Similar Rights that apply to Your use of the 64 | Licensed Material and that the Licensor has authority to license. 65 | 66 | j. Licensor means the individual(s) or entity(ies) granting rights 67 | under this Public License. 68 | 69 | k. NonCommercial means not primarily intended for or directed towards 70 | commercial advantage or monetary compensation. For purposes of 71 | this Public License, the exchange of the Licensed Material for 72 | other material subject to Copyright and Similar Rights by digital 73 | file-sharing or similar means is NonCommercial provided there is 74 | no payment of monetary compensation in connection with the 75 | exchange. 76 | 77 | l. Share means to provide material to the public by any means or 78 | process that requires permission under the Licensed Rights, such 79 | as reproduction, public display, public performance, distribution, 80 | dissemination, communication, or importation, and to make material 81 | available to the public including in ways that members of the 82 | public may access the material from a place and at a time 83 | individually chosen by them. 84 | 85 | m. Sui Generis Database Rights means rights other than copyright 86 | resulting from Directive 96/9/EC of the European Parliament and of 87 | the Council of 11 March 1996 on the legal protection of databases, 88 | as amended and/or succeeded, as well as other essentially 89 | equivalent rights anywhere in the world. 90 | 91 | n. You means the individual or entity exercising the Licensed Rights 92 | under this Public License. Your has a corresponding meaning. 93 | 94 | 95 | Section 2 -- Scope. 96 | 97 | a. License grant. 98 | 99 | 1. Subject to the terms and conditions of this Public License, 100 | the Licensor hereby grants You a worldwide, royalty-free, 101 | non-sublicensable, non-exclusive, irrevocable license to 102 | exercise the Licensed Rights in the Licensed Material to: 103 | 104 | a. reproduce and Share the Licensed Material, in whole or 105 | in part, for NonCommercial purposes only; and 106 | 107 | b. produce, reproduce, and Share Adapted Material for 108 | NonCommercial purposes only. 109 | 110 | 2. Exceptions and Limitations. For the avoidance of doubt, where 111 | Exceptions and Limitations apply to Your use, this Public 112 | License does not apply, and You do not need to comply with 113 | its terms and conditions. 114 | 115 | 3. Term. The term of this Public License is specified in Section 116 | 6(a). 117 | 118 | 4. Media and formats; technical modifications allowed. The 119 | Licensor authorizes You to exercise the Licensed Rights in 120 | all media and formats whether now known or hereafter created, 121 | and to make technical modifications necessary to do so. The 122 | Licensor waives and/or agrees not to assert any right or 123 | authority to forbid You from making technical modifications 124 | necessary to exercise the Licensed Rights, including 125 | technical modifications necessary to circumvent Effective 126 | Technological Measures. For purposes of this Public License, 127 | simply making modifications authorized by this Section 2(a) 128 | (4) never produces Adapted Material. 129 | 130 | 5. Downstream recipients. 131 | 132 | a. Offer from the Licensor -- Licensed Material. Every 133 | recipient of the Licensed Material automatically 134 | receives an offer from the Licensor to exercise the 135 | Licensed Rights under the terms and conditions of this 136 | Public License. 137 | 138 | b. Additional offer from the Licensor -- Adapted Material. 139 | Every recipient of Adapted Material from You 140 | automatically receives an offer from the Licensor to 141 | exercise the Licensed Rights in the Adapted Material 142 | under the conditions of the Adapter's License You apply. 143 | 144 | c. No downstream restrictions. You may not offer or impose 145 | any additional or different terms or conditions on, or 146 | apply any Effective Technological Measures to, the 147 | Licensed Material if doing so restricts exercise of the 148 | Licensed Rights by any recipient of the Licensed 149 | Material. 150 | 151 | 6. No endorsement. Nothing in this Public License constitutes or 152 | may be construed as permission to assert or imply that You 153 | are, or that Your use of the Licensed Material is, connected 154 | with, or sponsored, endorsed, or granted official status by, 155 | the Licensor or others designated to receive attribution as 156 | provided in Section 3(a)(1)(A)(i). 157 | 158 | b. Other rights. 159 | 160 | 1. Moral rights, such as the right of integrity, are not 161 | licensed under this Public License, nor are publicity, 162 | privacy, and/or other similar personality rights; however, to 163 | the extent possible, the Licensor waives and/or agrees not to 164 | assert any such rights held by the Licensor to the limited 165 | extent necessary to allow You to exercise the Licensed 166 | Rights, but not otherwise. 167 | 168 | 2. Patent and trademark rights are not licensed under this 169 | Public License. 170 | 171 | 3. To the extent possible, the Licensor waives any right to 172 | collect royalties from You for the exercise of the Licensed 173 | Rights, whether directly or through a collecting society 174 | under any voluntary or waivable statutory or compulsory 175 | licensing scheme. In all other cases the Licensor expressly 176 | reserves any right to collect such royalties, including when 177 | the Licensed Material is used other than for NonCommercial 178 | purposes. 179 | 180 | 181 | Section 3 -- License Conditions. 182 | 183 | Your exercise of the Licensed Rights is expressly made subject to the 184 | following conditions. 185 | 186 | a. Attribution. 187 | 188 | 1. If You Share the Licensed Material (including in modified 189 | form), You must: 190 | 191 | a. retain the following if it is supplied by the Licensor 192 | with the Licensed Material: 193 | 194 | i. identification of the creator(s) of the Licensed 195 | Material and any others designated to receive 196 | attribution, in any reasonable manner requested by 197 | the Licensor (including by pseudonym if 198 | designated); 199 | 200 | ii. a copyright notice; 201 | 202 | iii. a notice that refers to this Public License; 203 | 204 | iv. a notice that refers to the disclaimer of 205 | warranties; 206 | 207 | v. a URI or hyperlink to the Licensed Material to the 208 | extent reasonably practicable; 209 | 210 | b. indicate if You modified the Licensed Material and 211 | retain an indication of any previous modifications; and 212 | 213 | c. indicate the Licensed Material is licensed under this 214 | Public License, and include the text of, or the URI or 215 | hyperlink to, this Public License. 216 | 217 | 2. You may satisfy the conditions in Section 3(a)(1) in any 218 | reasonable manner based on the medium, means, and context in 219 | which You Share the Licensed Material. For example, it may be 220 | reasonable to satisfy the conditions by providing a URI or 221 | hyperlink to a resource that includes the required 222 | information. 223 | 3. If requested by the Licensor, You must remove any of the 224 | information required by Section 3(a)(1)(A) to the extent 225 | reasonably practicable. 226 | 227 | b. ShareAlike. 228 | 229 | In addition to the conditions in Section 3(a), if You Share 230 | Adapted Material You produce, the following conditions also apply. 231 | 232 | 1. The Adapter's License You apply must be a Creative Commons 233 | license with the same License Elements, this version or 234 | later, or a BY-NC-SA Compatible License. 235 | 236 | 2. You must include the text of, or the URI or hyperlink to, the 237 | Adapter's License You apply. You may satisfy this condition 238 | in any reasonable manner based on the medium, means, and 239 | context in which You Share Adapted Material. 240 | 241 | 3. You may not offer or impose any additional or different terms 242 | or conditions on, or apply any Effective Technological 243 | Measures to, Adapted Material that restrict exercise of the 244 | rights granted under the Adapter's License You apply. 245 | 246 | 247 | Section 4 -- Sui Generis Database Rights. 248 | 249 | Where the Licensed Rights include Sui Generis Database Rights that 250 | apply to Your use of the Licensed Material: 251 | 252 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 253 | to extract, reuse, reproduce, and Share all or a substantial 254 | portion of the contents of the database for NonCommercial purposes 255 | only; 256 | 257 | b. if You include all or a substantial portion of the database 258 | contents in a database in which You have Sui Generis Database 259 | Rights, then the database in which You have Sui Generis Database 260 | Rights (but not its individual contents) is Adapted Material, 261 | including for purposes of Section 3(b); and 262 | 263 | c. You must comply with the conditions in Section 3(a) if You Share 264 | all or a substantial portion of the contents of the database. 265 | 266 | For the avoidance of doubt, this Section 4 supplements and does not 267 | replace Your obligations under this Public License where the Licensed 268 | Rights include other Copyright and Similar Rights. 269 | 270 | 271 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 272 | 273 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 274 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 275 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 276 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 277 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 278 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 279 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 280 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 281 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 282 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 283 | 284 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 285 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 286 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 287 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 288 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 289 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 290 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 291 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 292 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 293 | 294 | c. The disclaimer of warranties and limitation of liability provided 295 | above shall be interpreted in a manner that, to the extent 296 | possible, most closely approximates an absolute disclaimer and 297 | waiver of all liability. 298 | 299 | 300 | Section 6 -- Term and Termination. 301 | 302 | a. This Public License applies for the term of the Copyright and 303 | Similar Rights licensed here. However, if You fail to comply with 304 | this Public License, then Your rights under this Public License 305 | terminate automatically. 306 | 307 | b. Where Your right to use the Licensed Material has terminated under 308 | Section 6(a), it reinstates: 309 | 310 | 1. automatically as of the date the violation is cured, provided 311 | it is cured within 30 days of Your discovery of the 312 | violation; or 313 | 314 | 2. upon express reinstatement by the Licensor. 315 | 316 | For the avoidance of doubt, this Section 6(b) does not affect any 317 | right the Licensor may have to seek remedies for Your violations 318 | of this Public License. 319 | 320 | c. For the avoidance of doubt, the Licensor may also offer the 321 | Licensed Material under separate terms or conditions or stop 322 | distributing the Licensed Material at any time; however, doing so 323 | will not terminate this Public License. 324 | 325 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 326 | License. 327 | 328 | 329 | Section 7 -- Other Terms and Conditions. 330 | 331 | a. The Licensor shall not be bound by any additional or different 332 | terms or conditions communicated by You unless expressly agreed. 333 | 334 | b. Any arrangements, understandings, or agreements regarding the 335 | Licensed Material not stated herein are separate from and 336 | independent of the terms and conditions of this Public License. 337 | 338 | 339 | Section 8 -- Interpretation. 340 | 341 | a. For the avoidance of doubt, this Public License does not, and 342 | shall not be interpreted to, reduce, limit, restrict, or impose 343 | conditions on any use of the Licensed Material that could lawfully 344 | be made without permission under this Public License. 345 | 346 | b. To the extent possible, if any provision of this Public License is 347 | deemed unenforceable, it shall be automatically reformed to the 348 | minimum extent necessary to make it enforceable. If the provision 349 | cannot be reformed, it shall be severed from this Public License 350 | without affecting the enforceability of the remaining terms and 351 | conditions. 352 | 353 | c. No term or condition of this Public License will be waived and no 354 | failure to comply consented to unless expressly agreed to by the 355 | Licensor. 356 | 357 | d. Nothing in this Public License constitutes or may be interpreted 358 | as a limitation upon, or waiver of, any privileges and immunities 359 | that apply to the Licensor or You, including from the legal 360 | processes of any jurisdiction or authority. 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluttergram 2 | A working Instagram clone written in Flutter using Firebase / Firestore 3 | 4 | ## Code Quality Disclaimer 5 | I built Fluttergram as a way to learn Flutter when it was still in Beta V1. At this time, Flutter was in its really early stages, and best practices were not yet established. As such, this repo **does not** reflect the current Flutter best practices and is not at a high code quality standard. That said, it still has merit in being a complete example of what can be done in Flutter. 6 | 7 | # Demo 8 | [Download the release APK to try out Fluttergram](https://github.com/mdanics/fluttergram/raw/master/app-release.apk) 9 | 10 | I update Fluttergram with new features and bugs fixes, but the apk may be behind master. Take a look at the [changelog](/CHANGELOG.md) to see the most recent additions to the apk. 11 | 12 | ## Features 13 | 14 | * Custom photo feed based on who you follow (using firebase cloud functions) 15 | * Post photo posts from camera or gallery 16 | * Like posts 17 | * Comment on posts 18 | * View all comments on a post 19 | * Search for users 20 | * Profile Pages 21 | * Follow / Unfollow Users 22 | * Change image view from grid layout to feed layout 23 | * Add your own bio 24 | * Activity Feed showing recent likes / comments of your posts + new followers 25 | 26 | ## Screenshots 27 |

28 | feed example 29 | upload photo example 30 | go to a profile from feed 31 | edit profile example 32 | comment and activity feed example 33 | 34 |

35 | 36 | ## Dependencies 37 | 38 | * [Flutter](https://flutter.dev/) 39 | * [Firestore](https://github.com/flutter/plugins/tree/master/packages/cloud_firestore) 40 | * [Image Picker](https://github.com/flutter/plugins/tree/master/packages/image_picker) 41 | * [Google Sign In](https://github.com/flutter/plugins/tree/master/packages/google_sign_in) 42 | * [Firebase Auth](https://github.com/flutter/plugins/tree/master/packages/firebase_auth) 43 | * [UUID](https://github.com/Daegalus/dart-uuid) 44 | * [Dart Image](https://github.com/brendan-duncan/image) 45 | * [Path Provider](https://github.com/flutter/plugins/tree/master/packages/path_provider) 46 | * [Font Awesome](https://github.com/brianegan/font_awesome_flutter) 47 | * [Dart HTTP](https://github.com/dart-lang/http) 48 | * [Dart Async](https://github.com/dart-lang/async) 49 | * [Flutter Shared Preferences]() 50 | * [Cached Network Image](https://github.com/renefloor/flutter_cached_network_image) 51 | 52 | ## Getting started 53 | 54 | #### 1. [Setup Flutter](https://flutter.dev/docs/get-started/install) 55 | 56 | #### 2. Clone the repo 57 | 58 | ```sh 59 | $ git clone https://github.com/mdanics/fluttergram.git 60 | $ cd fluttergram/ 61 | ``` 62 | 63 | #### 3. Setup the firebase app 64 | 65 | 1. You'll need to create a Firebase instance. Follow the instructions at https://console.firebase.google.com. 66 | 2. Once your Firebase instance is created, you'll need to enable Google authentication. 67 | 68 | * Go to the Firebase Console for your new instance. 69 | * Click "Authentication" in the left-hand menu 70 | * Click the "sign-in method" tab 71 | * Click "Google" and enable it 72 | 73 | 3. Create Cloud Functions (to make the Feed work) 74 | * Create a new firebase project with `firebase init` 75 | * Copy this project's `functions/lib/index.js` to your firebase project's `functions/index.js` 76 | * Push the function `getFeed` with `firebase deploy --only functions` In the output, you'll see the getFeed URL, copy that. 77 | * Replace the url in the `_getFeed` function in `feed.dart` with your cloud function url from the previous step. 78 | 79 | _**If this does not work** and you get the error `Error: Error parsing triggers: Cannot find module './notificationHandler'` Try following [these steps](https://github.com/mdanics/fluttergram/issues/25#issuecomment-434031430). If you are still unable to get it to work please open a new issue._ 80 | 81 | _You may need to create the neccessary index by running `firebase functions:log` and then clicking the link_ 82 | 83 | _**If you are getting no errors, but an empty feed** You must post photos or follow users with posts as the getFeed function only returns your own posts & posts from people you follow._ 84 | 85 | 4. Enable the Firebase Database 86 | * Go to the Firebase Console 87 | * Click "Database" in the left-hand menu 88 | * Click the Cloudstore "Create Database" button 89 | * Select "Start in test mode" and "Enable" 90 | 91 | 5. (skip if not running on Android) 92 | 93 | * Create an app within your Firebase instance for Android, with package name com.yourcompany.news 94 | * Run the following command to get your SHA-1 key: 95 | 96 | ``` 97 | keytool -exportcert -list -v \ 98 | -alias androiddebugkey -keystore ~/.android/debug.keystore 99 | ``` 100 | 101 | * In the Firebase console, in the settings of your Android app, add your SHA-1 key by clicking "Add Fingerprint". 102 | * Follow instructions to download google-services.json 103 | * place `google-services.json` into `/android/app/`. 104 | 105 | 6. (skip if not running on iOS) 106 | 107 | * Create an app within your Firebase instance for iOS, with your app package name 108 | * Follow instructions to download GoogleService-Info.plist 109 | * Open XCode, right click the Runner folder, select the "Add Files to 'Runner'" menu, and select the GoogleService-Info.plist file to add it to /ios/Runner in XCode 110 | * Open /ios/Runner/Info.plist in a text editor. Locate the CFBundleURLSchemes key. The second item in the array value of this key is specific to the Firebase instance. Replace it with the value for REVERSED_CLIENT_ID from GoogleService-Info.plist 111 | 112 | Double check install instructions for both 113 | - Google Auth Plugin 114 | - https://pub.dartlang.org/packages/firebase_auth 115 | - Firestore Plugin 116 | - https://pub.dartlang.org/packages/cloud_firestore 117 | 118 | # What's Next? 119 | - [x] Notificaitons for likes, comments, follows, etc 120 | - [X] Animations (heart when liking image) [(#77)](https://github.com/mdanics/fluttergram/pull/77) 121 | - [ ] Improve Caching of Profiles, Images, Etc. 122 | - [ ] Better post creation, add filters to your image 123 | - [ ] Custom Camera Implementation 124 | - [ ] Firebase Security Rules 125 | - [ ] Delete Posts 126 | - [ ] Direct Messaging 127 | - [ ] Stories 128 | - [ ] Clean up code 129 | -------------------------------------------------------------------------------- /android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | /app/google-services.json 12 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 30 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 26 | applicationId "me.danics.firestoretest" 27 | minSdkVersion 21 28 | targetSdkVersion 27 29 | versionCode 1 30 | versionName "1.2.3" 31 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 32 | multiDexEnabled true 33 | } 34 | 35 | buildTypes { 36 | release { 37 | // TODO: Add your own signing config for the release build. 38 | // Signing with the debug keys for now, so `flutter run --release` works. 39 | signingConfig signingConfigs.debug 40 | } 41 | } 42 | } 43 | 44 | flutter { 45 | source '../..' 46 | } 47 | 48 | dependencies { 49 | testImplementation 'junit:junit:4.12' 50 | androidTestImplementation 'androidx.test:runner:1.1.0-alpha4' 51 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4' 52 | implementation 'com.google.firebase:firebase-core:16.0.4' 53 | 54 | } 55 | apply plugin: 'com.google.gms.google-services' 56 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 21 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 52 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /android/app/src/main/java/me/danics/firestoretest/MainActivity.java: -------------------------------------------------------------------------------- 1 | package me.danics.firestoretest; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.3.2' 9 | classpath 'com.google.gms:google-services:4.3.0' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | maven { 18 | url "https://maven.google.com" // Google's Maven repository 19 | } 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 02 01:36:58 EDT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/app-release.apk -------------------------------------------------------------------------------- /assets/fonts/Billabong.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/assets/fonts/Billabong.ttf -------------------------------------------------------------------------------- /assets/images/google_signin_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/assets/images/google_signin_button.png -------------------------------------------------------------------------------- /firestore_test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /firestore_test_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /functions/lib/getFeed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const admin = require("firebase-admin"); 12 | exports.getFeedModule = function (req, res) { 13 | const uid = String(req.query.uid); 14 | function compileFeedPost() { 15 | return __awaiter(this, void 0, void 0, function* () { 16 | const following = yield getFollowing(uid, res); 17 | let listOfPosts = yield getAllPosts(following, uid, res); 18 | listOfPosts = [].concat.apply([], listOfPosts); // flattens list 19 | res.send(listOfPosts); 20 | }); 21 | } 22 | compileFeedPost().then().catch(); 23 | }; 24 | function getAllPosts(following, uid, res) { 25 | return __awaiter(this, void 0, void 0, function* () { 26 | const listOfPosts = []; 27 | for (const user in following) { 28 | listOfPosts.push(yield getUserPosts(following[user], res)); 29 | } 30 | // add the current user's posts to the feed so that your own posts appear in your feed 31 | listOfPosts.push(yield getUserPosts(uid, res)); 32 | return listOfPosts; 33 | }); 34 | } 35 | function getUserPosts(userId, res) { 36 | const posts = admin.firestore().collection("insta_posts").where("ownerId", "==", userId).orderBy("timestamp"); 37 | return posts.get() 38 | .then(function (querySnapshot) { 39 | const listOfPosts = []; 40 | querySnapshot.forEach(function (doc) { 41 | listOfPosts.push(doc.data()); 42 | }); 43 | return listOfPosts; 44 | }); 45 | } 46 | function getFollowing(uid, res) { 47 | const doc = admin.firestore().doc(`insta_users/${uid}`); 48 | return doc.get().then(snapshot => { 49 | const followings = snapshot.data().following; 50 | const following_list = []; 51 | for (const following in followings) { 52 | if (followings[following] === true) { 53 | following_list.push(following); 54 | } 55 | } 56 | return following_list; 57 | }).catch(error => { 58 | res.status(500).send(error); 59 | }); 60 | } 61 | //# sourceMappingURL=getFeed.js.map -------------------------------------------------------------------------------- /functions/lib/getFeed.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"getFeed.js","sourceRoot":"","sources":["../src/getFeed.ts"],"names":[],"mappings":";;;;;;;;;;AACA,wCAAwC;AAG3B,QAAA,aAAa,GAAG,UAAS,GAAG,EAAE,GAAG;IAC1C,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAElC;;YACE,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,GAAG,CAAQ,CAAC;YAEtD,IAAI,WAAW,GAAG,MAAM,WAAW,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAEzD,WAAW,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,gBAAgB;YAEhE,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACxB,CAAC;KAAA;IAED,eAAe,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;AACrC,CAAC,CAAA;AAED,qBAA2B,SAAS,EAAE,GAAG,EAAE,GAAG;;QAC1C,MAAM,WAAW,GAAG,EAAE,CAAC;QAEvB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAC;YACzB,WAAW,CAAC,IAAI,CAAE,MAAM,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;SAC/D;QAED,sFAAsF;QACtF,WAAW,CAAC,IAAI,CAAE,MAAM,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAEhD,OAAO,WAAW,CAAC;IACvB,CAAC;CAAA;AAED,sBAAsB,MAAM,EAAE,GAAG;IAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAE7G,OAAO,KAAK,CAAC,GAAG,EAAE;SACjB,IAAI,CAAC,UAAS,aAAa;QACxB,MAAM,WAAW,GAAG,EAAE,CAAC;QAEvB,aAAa,CAAC,OAAO,CAAC,UAAS,GAAG;YAC9B,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC;IACvB,CAAC,CAAC,CAAA;AACN,CAAC;AAGD,sBAAsB,GAAG,EAAE,GAAG;IAC1B,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,eAAe,GAAG,EAAE,CAAC,CAAA;IACvD,OAAO,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;QAC/B,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC;QAE7C,MAAM,cAAc,GAAG,EAAE,CAAC;QAE1B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;YAClC,IAAI,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,EAAC;gBACjC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;aAChC;SACF;QACD,OAAO,cAAc,CAAC;IAC1B,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;QACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;AACN,CAAC"} -------------------------------------------------------------------------------- /functions/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const functions = require("firebase-functions"); 12 | const admin = require("firebase-admin"); 13 | const notificationHandler_1 = require("./lib/notificationHandler"); 14 | const getFeed_1 = require("./lib/getFeed"); 15 | admin.initializeApp(); 16 | exports.notificationHandler = functions.firestore.document("/insta_a_feed/{userId}/items/{activityFeedItem}") 17 | .onCreate((snapshot, context) => __awaiter(this, void 0, void 0, function* () { 18 | yield notificationHandler_1.notificationHandlerModule(snapshot, context); 19 | })); 20 | exports.getFeed = functions.https.onRequest((req, res) => { 21 | getFeed_1.getFeedModule(req, res); 22 | }); 23 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /functions/lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,gDAAgD;AAChD,wCAAwC;AACxC,+DAAiE;AACjE,uCAAyC;AACzC,KAAK,CAAC,aAAa,EAAE,CAAC;AAET,QAAA,mBAAmB,GAAG,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,iDAAiD,CAAC;KAC7G,QAAQ,CAAC,CAAO,QAAQ,EAAE,OAAO,EAAE,EAAE;IACnC,MAAM,+CAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC,CAAA,CAAC,CAAC;AAGM,QAAA,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC5D,uBAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAC1B,CAAC,CAAC,CAAA"} -------------------------------------------------------------------------------- /functions/lib/notificationHandler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const admin = require("firebase-admin"); 12 | exports.notificationHandlerModule = function (snapshot, context) { 13 | return __awaiter(this, void 0, void 0, function* () { 14 | console.log(snapshot.data()); 15 | const ownerDoc = admin.firestore().doc("insta_users/" + context.params.userId); 16 | const ownerData = yield ownerDoc.get(); 17 | const androidNotificationToken = ownerData.data()["androidNotificationToken"]; 18 | if (androidNotificationToken) { 19 | sendNotification(androidNotificationToken, snapshot.data()); 20 | } 21 | else { 22 | console.log("No token for User, not sending a notification"); 23 | } 24 | return 0; 25 | }); 26 | }; 27 | function sendNotification(androidNotificationToken, activityItem) { 28 | let title; 29 | let body = ""; 30 | if (activityItem['type'] === "comment") { 31 | title = "New Comment"; 32 | body = activityItem['username'] + " commented on your post: " + activityItem["commentData"]; 33 | } 34 | else if (activityItem["type"] === "like") { 35 | title = activityItem['username'] + " liked your post"; 36 | } 37 | else if (activityItem["type"] === "follow") { 38 | title = activityItem['username'] + " started following you"; 39 | } 40 | const message = { 41 | notification: { 42 | title: title, 43 | body: body 44 | }, 45 | token: androidNotificationToken 46 | }; 47 | admin.messaging().send(message) 48 | .then((response) => { 49 | // Response is a message ID string. 50 | console.log('Successfully sent message:', response); 51 | }) 52 | .catch((error) => { 53 | console.log('Error sending message:', error); 54 | }); 55 | } 56 | //# sourceMappingURL=notificationHandler.js.map -------------------------------------------------------------------------------- /functions/lib/notificationHandler.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"notificationHandler.js","sourceRoot":"","sources":["../src/notificationHandler.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,wCAAwC;AAG3B,QAAA,yBAAyB,GAAG,UAAgB,QAAQ,EAAE,OAAO;;QACpE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;QAE5B,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC9E,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAA;QAItC,MAAM,wBAAwB,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,0BAA0B,CAAC,CAAC;QAC9E,IAAI,wBAAwB,EAAE;YAC3B,gBAAgB,CAAC,wBAAwB,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;SAG9D;aAAM;YACL,OAAO,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC;SAC9D;QAED,OAAO,CAAC,CAAC;IACX,CAAC;CAAA,CAAC;AAEN,0BAA0B,wBAAgC,EAAE,YAA4C;IAEpG,IAAI,KAAa,CAAC;IAClB,IAAI,IAAI,GAAW,EAAE,CAAC;IAEtB,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,SAAS,EAAE;QACtC,KAAK,GAAG,aAAa,CAAA;QACrB,IAAI,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG,2BAA2B,GAAG,YAAY,CAAC,aAAa,CAAC,CAAA;KAC5F;SAAM,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,MAAM,EAAE;QAC1C,KAAK,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG,kBAAkB,CAAA;KACtD;SAAM,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAC;QAC3C,KAAK,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG,wBAAwB,CAAA;KAC5D;IAED,MAAM,OAAO,GAAG;QACd,YAAY,EAAE;YACZ,KAAK,EAAE,KAAK;YACZ,IAAI,EAAE,IAAI;SACX;QACD,KAAK,EAAE,wBAAwB;KAChC,CAAA;IAED,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;SAC9B,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;QACjB,mCAAmC;QACnC,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,QAAQ,CAAC,CAAC;IACtD,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QACf,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC"} -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc --skipLibCheck ", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "main": "lib/index.js", 13 | "dependencies": { 14 | "firebase-admin": "^6.1.0", 15 | "firebase-functions": "^2.1.0" 16 | }, 17 | "devDependencies": { 18 | "tslint": "^5.8.0", 19 | "typescript": "^2.5.3" 20 | }, 21 | "private": true, 22 | "engines": { 23 | "node": "8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /functions/src/getFeed.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import * as admin from 'firebase-admin'; 3 | 4 | 5 | export const getFeedModule = function(req, res) { 6 | const uid = String(req.query.uid); 7 | 8 | async function compileFeedPost() { 9 | const following = await getFollowing(uid, res) as any; 10 | 11 | let listOfPosts = await getAllPosts(following, uid, res); 12 | 13 | listOfPosts = [].concat.apply([], listOfPosts); // flattens list 14 | 15 | res.send(listOfPosts); 16 | } 17 | 18 | compileFeedPost().then().catch(); 19 | } 20 | 21 | async function getAllPosts(following, uid, res) { 22 | const listOfPosts = []; 23 | 24 | for (const user in following){ 25 | listOfPosts.push( await getUserPosts(following[user], res)); 26 | } 27 | 28 | // add the current user's posts to the feed so that your own posts appear in your feed 29 | listOfPosts.push( await getUserPosts(uid, res)); 30 | 31 | return listOfPosts; 32 | } 33 | 34 | function getUserPosts(userId, res){ 35 | const posts = admin.firestore().collection("insta_posts").where("ownerId", "==", userId).orderBy("timestamp") 36 | 37 | return posts.get() 38 | .then(function(querySnapshot) { 39 | const listOfPosts = []; 40 | 41 | querySnapshot.forEach(function(doc) { 42 | listOfPosts.push(doc.data()); 43 | }); 44 | 45 | return listOfPosts; 46 | }) 47 | } 48 | 49 | 50 | function getFollowing(uid, res){ 51 | const doc = admin.firestore().doc(`insta_users/${uid}`) 52 | return doc.get().then(snapshot => { 53 | const followings = snapshot.data().following; 54 | 55 | const following_list = []; 56 | 57 | for (const following in followings) { 58 | if (followings[following] === true){ 59 | following_list.push(following); 60 | } 61 | } 62 | return following_list; 63 | }).catch(error => { 64 | res.status(500).send(error) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import * as admin from 'firebase-admin'; 3 | import { notificationHandlerModule } from "./notificationHandler" 4 | import { getFeedModule } from "./getFeed" 5 | admin.initializeApp(); 6 | 7 | export const notificationHandler = functions.firestore.document("/insta_a_feed/{userId}/items/{activityFeedItem}") 8 | .onCreate(async (snapshot, context) => { 9 | await notificationHandlerModule(snapshot, context); 10 | }); 11 | 12 | 13 | export const getFeed = functions.https.onRequest((req, res) => { 14 | getFeedModule(req, res); 15 | }) -------------------------------------------------------------------------------- /functions/src/notificationHandler.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin'; 2 | import { DocumentSnapshot } from 'firebase-functions/lib/providers/firestore'; 3 | 4 | export const notificationHandlerModule = async function (snapshot, context) { 5 | console.log(snapshot.data()) 6 | 7 | const ownerDoc = admin.firestore().doc("insta_users/" + context.params.userId) 8 | const ownerData = await ownerDoc.get() 9 | 10 | 11 | 12 | const androidNotificationToken = ownerData.data()["androidNotificationToken"]; 13 | if (androidNotificationToken) { 14 | sendNotification(androidNotificationToken, snapshot.data()); 15 | 16 | 17 | } else { 18 | console.log("No token for User, not sending a notification"); 19 | } 20 | 21 | return 0; 22 | }; 23 | 24 | function sendNotification(androidNotificationToken: string, activityItem: FirebaseFirestore.DocumentData) { 25 | 26 | let title: string; 27 | let body: string = ""; 28 | 29 | if (activityItem['type'] === "comment") { 30 | title = "New Comment" 31 | body = activityItem['username'] + " commented on your post: " + activityItem["commentData"] 32 | } else if (activityItem["type"] === "like") { 33 | title = activityItem['username'] + " liked your post" 34 | } else if (activityItem["type"] === "follow"){ 35 | title = activityItem['username'] + " started following you" 36 | } 37 | 38 | const message = { 39 | notification: { 40 | title: title, 41 | body: body 42 | }, 43 | token: androidNotificationToken 44 | } 45 | 46 | admin.messaging().send(message) 47 | .then((response) => { 48 | // Response is a message ID string. 49 | console.log('Successfully sent message:', response); 50 | }) 51 | .catch((error) => { 52 | console.log('Error sending message:', error); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Do not allow variables to be used before they are declared. 69 | "no-use-before-declare": true, 70 | 71 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 72 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 73 | 74 | // Disallow duplicate imports in the same file. 75 | "no-duplicate-imports": true, 76 | 77 | 78 | // -- Strong Warnings -- 79 | // These rules should almost never be needed, but may be included due to legacy code. 80 | // They are left as a warning to avoid frustration with blocked deploys when the developer 81 | // understand the warning and wants to deploy anyway. 82 | 83 | // Warn when an empty interface is defined. These are generally not useful. 84 | "no-empty-interface": {"severity": "warning"}, 85 | 86 | // Warn when an import will have side effects. 87 | "no-import-side-effect": {"severity": "warning"}, 88 | 89 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 90 | // most values and let for values that will change. 91 | "no-var-keyword": {"severity": "warning"}, 92 | 93 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 94 | "triple-equals": {"severity": "warning"}, 95 | 96 | // Warn when using deprecated APIs. 97 | "deprecation": {"severity": "warning"}, 98 | 99 | // -- Light Warnigns -- 100 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 101 | // if TSLint supported such a level. 102 | 103 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 104 | // (Even better: check out utils like .map if transforming an array!) 105 | "prefer-for-of": {"severity": "warning"}, 106 | 107 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 108 | "unified-signatures": {"severity": "warning"}, 109 | 110 | // Warns if code has an import or variable that is unused. 111 | "no-unused-variable": {"severity": "warning"}, 112 | 113 | // Prefer const for values that will not change. This better documents code. 114 | "prefer-const": {"severity": "warning"}, 115 | 116 | // Multi-line object liiterals and function calls should have a trailing comma. This helps avoid merge conflicts. 117 | "trailing-comma": {"severity": "warning"} 118 | }, 119 | 120 | "defaultSeverity": "error" 121 | } 122 | -------------------------------------------------------------------------------- /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/flutter_assets/ 37 | /Flutter/App.framework 38 | /Flutter/Flutter.framework 39 | /Flutter/Generated.xcconfig 40 | /ServiceDefinitions.json 41 | 42 | Pods/ 43 | -------------------------------------------------------------------------------- /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 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /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 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 2FEA05412183A21600873DEA /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2FEA05402183A21600873DEA /* GoogleService-Info.plist */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 14 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 15 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 16 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 17 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 18 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 19 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 20 | F40AB5A65EEA901E84DF62F2 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A48A54A8F3D8BA3C696BF983 /* libPods-Runner.a */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXCopyFilesBuildPhase section */ 24 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 25 | isa = PBXCopyFilesBuildPhase; 26 | buildActionMask = 2147483647; 27 | dstPath = ""; 28 | dstSubfolderSpec = 10; 29 | files = ( 30 | ); 31 | name = "Embed Frameworks"; 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXCopyFilesBuildPhase section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 38 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 39 | 172F131D775F5297C2841A96 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 40 | 2FEA05402183A21600873DEA /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 41 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 42 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 43 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 44 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 45 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 46 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 47 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 49 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | A47AA0D76093A7A7D14D1F88 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 54 | A48A54A8F3D8BA3C696BF983 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | F40AB5A65EEA901E84DF62F2 /* libPods-Runner.a in Frameworks */, 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | 7CB42A7162B3851135A6C52A /* Pods */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | A47AA0D76093A7A7D14D1F88 /* Pods-Runner.debug.xcconfig */, 73 | 172F131D775F5297C2841A96 /* Pods-Runner.release.xcconfig */, 74 | ); 75 | name = Pods; 76 | sourceTree = ""; 77 | }; 78 | 9740EEB11CF90186004384FC /* Flutter */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 82 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 83 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 84 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 85 | ); 86 | name = Flutter; 87 | sourceTree = ""; 88 | }; 89 | 97C146E51CF9000F007C117D = { 90 | isa = PBXGroup; 91 | children = ( 92 | 2FEA05402183A21600873DEA /* GoogleService-Info.plist */, 93 | 9740EEB11CF90186004384FC /* Flutter */, 94 | 97C146F01CF9000F007C117D /* Runner */, 95 | 97C146EF1CF9000F007C117D /* Products */, 96 | 7CB42A7162B3851135A6C52A /* Pods */, 97 | B60D479C97BD6EB628333B42 /* Frameworks */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | 97C146EF1CF9000F007C117D /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 97C146EE1CF9000F007C117D /* Runner.app */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | 97C146F01CF9000F007C117D /* Runner */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 113 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 114 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 115 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 116 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 117 | 97C147021CF9000F007C117D /* Info.plist */, 118 | 97C146F11CF9000F007C117D /* Supporting Files */, 119 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 120 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 121 | ); 122 | path = Runner; 123 | sourceTree = ""; 124 | }; 125 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 97C146F21CF9000F007C117D /* main.m */, 129 | ); 130 | name = "Supporting Files"; 131 | sourceTree = ""; 132 | }; 133 | B60D479C97BD6EB628333B42 /* Frameworks */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | A48A54A8F3D8BA3C696BF983 /* libPods-Runner.a */, 137 | ); 138 | name = Frameworks; 139 | sourceTree = ""; 140 | }; 141 | /* End PBXGroup section */ 142 | 143 | /* Begin PBXNativeTarget section */ 144 | 97C146ED1CF9000F007C117D /* Runner */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 147 | buildPhases = ( 148 | 0AB6CFB15BD839216C341DA3 /* [CP] Check Pods Manifest.lock */, 149 | 9740EEB61CF901F6004384FC /* Run Script */, 150 | 97C146EA1CF9000F007C117D /* Sources */, 151 | 97C146EB1CF9000F007C117D /* Frameworks */, 152 | 97C146EC1CF9000F007C117D /* Resources */, 153 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 154 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 155 | D2C1B3B5A3B05D71F659340E /* [CP] Embed Pods Frameworks */, 156 | 11EDB517A9D814A7C2BF386E /* [CP] Copy Pods Resources */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = Runner; 163 | productName = Runner; 164 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 165 | productType = "com.apple.product-type.application"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | 97C146E61CF9000F007C117D /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | LastUpgradeCheck = 0910; 174 | ORGANIZATIONNAME = "The Chromium Authors"; 175 | TargetAttributes = { 176 | 97C146ED1CF9000F007C117D = { 177 | CreatedOnToolsVersion = 7.3.1; 178 | DevelopmentTeam = XFKDZYYPTU; 179 | ProvisioningStyle = Automatic; 180 | }; 181 | }; 182 | }; 183 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 184 | compatibilityVersion = "Xcode 3.2"; 185 | developmentRegion = English; 186 | hasScannedForEncodings = 0; 187 | knownRegions = ( 188 | English, 189 | en, 190 | Base, 191 | ); 192 | mainGroup = 97C146E51CF9000F007C117D; 193 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 194 | projectDirPath = ""; 195 | projectRoot = ""; 196 | targets = ( 197 | 97C146ED1CF9000F007C117D /* Runner */, 198 | ); 199 | }; 200 | /* End PBXProject section */ 201 | 202 | /* Begin PBXResourcesBuildPhase section */ 203 | 97C146EC1CF9000F007C117D /* Resources */ = { 204 | isa = PBXResourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 208 | 2FEA05412183A21600873DEA /* GoogleService-Info.plist in Resources */, 209 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 210 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 211 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 212 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 213 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXResourcesBuildPhase section */ 218 | 219 | /* Begin PBXShellScriptBuildPhase section */ 220 | 0AB6CFB15BD839216C341DA3 /* [CP] Check Pods Manifest.lock */ = { 221 | isa = PBXShellScriptBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | ); 225 | inputPaths = ( 226 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 227 | "${PODS_ROOT}/Manifest.lock", 228 | ); 229 | name = "[CP] Check Pods Manifest.lock"; 230 | outputPaths = ( 231 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | shellPath = /bin/sh; 235 | 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"; 236 | showEnvVarsInLog = 0; 237 | }; 238 | 11EDB517A9D814A7C2BF386E /* [CP] Copy Pods Resources */ = { 239 | isa = PBXShellScriptBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | ); 243 | inputPaths = ( 244 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", 245 | "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", 246 | "${PODS_CONFIGURATION_BUILD_DIR}/gRPC-C++/gRPCCertificates-Cpp.bundle", 247 | ); 248 | name = "[CP] Copy Pods Resources"; 249 | outputPaths = ( 250 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", 251 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Cpp.bundle", 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | shellPath = /bin/sh; 255 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; 256 | showEnvVarsInLog = 0; 257 | }; 258 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 259 | isa = PBXShellScriptBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | inputPaths = ( 264 | ); 265 | name = "Thin Binary"; 266 | outputPaths = ( 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | shellPath = /bin/sh; 270 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 271 | }; 272 | 9740EEB61CF901F6004384FC /* Run Script */ = { 273 | isa = PBXShellScriptBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | ); 277 | inputPaths = ( 278 | ); 279 | name = "Run Script"; 280 | outputPaths = ( 281 | ); 282 | runOnlyForDeploymentPostprocessing = 0; 283 | shellPath = /bin/sh; 284 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 285 | }; 286 | D2C1B3B5A3B05D71F659340E /* [CP] Embed Pods Frameworks */ = { 287 | isa = PBXShellScriptBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | ); 291 | inputPaths = ( 292 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", 293 | "${PODS_ROOT}/../Flutter/Flutter.framework", 294 | ); 295 | name = "[CP] Embed Pods Frameworks"; 296 | outputPaths = ( 297 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | shellPath = /bin/sh; 301 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 302 | showEnvVarsInLog = 0; 303 | }; 304 | /* End PBXShellScriptBuildPhase section */ 305 | 306 | /* Begin PBXSourcesBuildPhase section */ 307 | 97C146EA1CF9000F007C117D /* Sources */ = { 308 | isa = PBXSourcesBuildPhase; 309 | buildActionMask = 2147483647; 310 | files = ( 311 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 312 | 97C146F31CF9000F007C117D /* main.m in Sources */, 313 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | /* End PBXSourcesBuildPhase section */ 318 | 319 | /* Begin PBXVariantGroup section */ 320 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 321 | isa = PBXVariantGroup; 322 | children = ( 323 | 97C146FB1CF9000F007C117D /* Base */, 324 | ); 325 | name = Main.storyboard; 326 | sourceTree = ""; 327 | }; 328 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 329 | isa = PBXVariantGroup; 330 | children = ( 331 | 97C147001CF9000F007C117D /* Base */, 332 | ); 333 | name = LaunchScreen.storyboard; 334 | sourceTree = ""; 335 | }; 336 | /* End PBXVariantGroup section */ 337 | 338 | /* Begin XCBuildConfiguration section */ 339 | 97C147031CF9000F007C117D /* Debug */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_COMMA = YES; 351 | CLANG_WARN_CONSTANT_CONVERSION = YES; 352 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 353 | CLANG_WARN_EMPTY_BODY = YES; 354 | CLANG_WARN_ENUM_CONVERSION = YES; 355 | CLANG_WARN_INFINITE_RECURSION = YES; 356 | CLANG_WARN_INT_CONVERSION = YES; 357 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 358 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 360 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 361 | CLANG_WARN_STRICT_PROTOTYPES = YES; 362 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 363 | CLANG_WARN_UNREACHABLE_CODE = YES; 364 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 365 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 366 | COPY_PHASE_STRIP = NO; 367 | DEBUG_INFORMATION_FORMAT = dwarf; 368 | ENABLE_STRICT_OBJC_MSGSEND = YES; 369 | ENABLE_TESTABILITY = YES; 370 | GCC_C_LANGUAGE_STANDARD = gnu99; 371 | GCC_DYNAMIC_NO_PIC = NO; 372 | GCC_NO_COMMON_BLOCKS = YES; 373 | GCC_OPTIMIZATION_LEVEL = 0; 374 | GCC_PREPROCESSOR_DEFINITIONS = ( 375 | "DEBUG=1", 376 | "$(inherited)", 377 | ); 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 385 | MTL_ENABLE_DEBUG_INFO = YES; 386 | ONLY_ACTIVE_ARCH = YES; 387 | SDKROOT = iphoneos; 388 | TARGETED_DEVICE_FAMILY = "1,2"; 389 | }; 390 | name = Debug; 391 | }; 392 | 97C147041CF9000F007C117D /* Release */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ALWAYS_SEARCH_USER_PATHS = NO; 396 | CLANG_ANALYZER_NONNULL = YES; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 398 | CLANG_CXX_LIBRARY = "libc++"; 399 | CLANG_ENABLE_MODULES = YES; 400 | CLANG_ENABLE_OBJC_ARC = YES; 401 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 402 | CLANG_WARN_BOOL_CONVERSION = YES; 403 | CLANG_WARN_COMMA = YES; 404 | CLANG_WARN_CONSTANT_CONVERSION = YES; 405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 406 | CLANG_WARN_EMPTY_BODY = YES; 407 | CLANG_WARN_ENUM_CONVERSION = YES; 408 | CLANG_WARN_INFINITE_RECURSION = YES; 409 | CLANG_WARN_INT_CONVERSION = YES; 410 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 413 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 414 | CLANG_WARN_STRICT_PROTOTYPES = YES; 415 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 416 | CLANG_WARN_UNREACHABLE_CODE = YES; 417 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 418 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 419 | COPY_PHASE_STRIP = NO; 420 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 421 | ENABLE_NS_ASSERTIONS = NO; 422 | ENABLE_STRICT_OBJC_MSGSEND = YES; 423 | GCC_C_LANGUAGE_STANDARD = gnu99; 424 | GCC_NO_COMMON_BLOCKS = YES; 425 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 426 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 427 | GCC_WARN_UNDECLARED_SELECTOR = YES; 428 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 429 | GCC_WARN_UNUSED_FUNCTION = YES; 430 | GCC_WARN_UNUSED_VARIABLE = YES; 431 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 432 | MTL_ENABLE_DEBUG_INFO = NO; 433 | SDKROOT = iphoneos; 434 | TARGETED_DEVICE_FAMILY = "1,2"; 435 | VALIDATE_PRODUCT = YES; 436 | }; 437 | name = Release; 438 | }; 439 | 97C147061CF9000F007C117D /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 442 | buildSettings = { 443 | ARCHS = arm64; 444 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 445 | CODE_SIGN_IDENTITY = "iPhone Developer"; 446 | CODE_SIGN_STYLE = Automatic; 447 | DEVELOPMENT_TEAM = XFKDZYYPTU; 448 | ENABLE_BITCODE = NO; 449 | FRAMEWORK_SEARCH_PATHS = ( 450 | "$(inherited)", 451 | "$(PROJECT_DIR)/Flutter", 452 | ); 453 | INFOPLIST_FILE = Runner/Info.plist; 454 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 455 | LIBRARY_SEARCH_PATHS = ( 456 | "$(inherited)", 457 | "$(PROJECT_DIR)/Flutter", 458 | ); 459 | PRODUCT_BUNDLE_IDENTIFIER = me.danics.fluttergram; 460 | PRODUCT_NAME = "$(TARGET_NAME)"; 461 | PROVISIONING_PROFILE = ""; 462 | PROVISIONING_PROFILE_SPECIFIER = ""; 463 | }; 464 | name = Debug; 465 | }; 466 | 97C147071CF9000F007C117D /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 469 | buildSettings = { 470 | ARCHS = arm64; 471 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 472 | CODE_SIGN_IDENTITY = "iPhone Developer"; 473 | CODE_SIGN_STYLE = Automatic; 474 | DEVELOPMENT_TEAM = XFKDZYYPTU; 475 | ENABLE_BITCODE = NO; 476 | FRAMEWORK_SEARCH_PATHS = ( 477 | "$(inherited)", 478 | "$(PROJECT_DIR)/Flutter", 479 | ); 480 | INFOPLIST_FILE = Runner/Info.plist; 481 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 482 | LIBRARY_SEARCH_PATHS = ( 483 | "$(inherited)", 484 | "$(PROJECT_DIR)/Flutter", 485 | ); 486 | PRODUCT_BUNDLE_IDENTIFIER = me.danics.fluttergram; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | PROVISIONING_PROFILE_SPECIFIER = ""; 489 | }; 490 | name = Release; 491 | }; 492 | /* End XCBuildConfiguration section */ 493 | 494 | /* Begin XCConfigurationList section */ 495 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 496 | isa = XCConfigurationList; 497 | buildConfigurations = ( 498 | 97C147031CF9000F007C117D /* Debug */, 499 | 97C147041CF9000F007C117D /* Release */, 500 | ); 501 | defaultConfigurationIsVisible = 0; 502 | defaultConfigurationName = Release; 503 | }; 504 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 505 | isa = XCConfigurationList; 506 | buildConfigurations = ( 507 | 97C147061CF9000F007C117D /* Debug */, 508 | 97C147071CF9000F007C117D /* Release */, 509 | ); 510 | defaultConfigurationIsVisible = 0; 511 | defaultConfigurationName = Release; 512 | }; 513 | /* End XCConfigurationList section */ 514 | }; 515 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 516 | } 517 | -------------------------------------------------------------------------------- /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 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdanics/fluttergram/d372cfd8e34994af5f774a25b3d4ae147d3965aa/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | Fluttergram 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | firestore_test 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLSchemes 29 | 30 | com.googleusercontent.apps.7019780124-8d9k97bo7jd6gn2fdm8pahj260br014j 31 | com.googleusercontent.apps.7019780124-lrego1vac3jl8a6egvo8efe56o8v2po0 32 | 33 | 34 | 35 | CFBundleVersion 36 | 1 37 | LSRequiresIPhoneOS 38 | 39 | NSCameraUsageDescription 40 | Used to allow the user to take a photo to upload as a post 41 | NSPhotoLibraryUsageDescription 42 | Used to allow the user to select a photo to upload as a post 43 | UILaunchStoryboardName 44 | LaunchScreen 45 | UIMainStoryboardFile 46 | Main 47 | UIRequiredDeviceCapabilities 48 | 49 | arm64 50 | 51 | UISupportedInterfaceOrientations 52 | 53 | UIInterfaceOrientationPortrait 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | UISupportedInterfaceOrientations~ipad 58 | 59 | UIInterfaceOrientationPortrait 60 | UIInterfaceOrientationPortraitUpsideDown 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | NSLocationAlwaysUsageDescription 65 | Your location required to post 66 | NSLocationWhenInUseUsageDescription 67 | Your location required to post 68 | UIViewControllerBasedStatusBarAppearance 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /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, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/activity_feed.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'image_post.dart'; //needed to open image when clicked 4 | import 'profile_page.dart'; // to open the profile page when username clicked 5 | import 'main.dart'; //needed for currentuser id 6 | 7 | class ActivityFeedPage extends StatefulWidget { 8 | @override 9 | _ActivityFeedPageState createState() => _ActivityFeedPageState(); 10 | } 11 | 12 | class _ActivityFeedPageState extends State with AutomaticKeepAliveClientMixin { 13 | @override 14 | Widget build(BuildContext context) { 15 | super.build(context); // reloads state when opened again 16 | 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: Text( 20 | "Activity Feed", 21 | style: TextStyle(color: Colors.black), 22 | ), 23 | backgroundColor: Colors.white, 24 | ), 25 | body: buildActivityFeed(), 26 | ); 27 | } 28 | 29 | buildActivityFeed() { 30 | return Container( 31 | child: FutureBuilder( 32 | future: getFeed(), 33 | builder: (context, snapshot) { 34 | if (!snapshot.hasData) 35 | return Container( 36 | alignment: FractionalOffset.center, 37 | padding: const EdgeInsets.only(top: 10.0), 38 | child: CircularProgressIndicator()); 39 | else { 40 | return ListView(children: snapshot.data); 41 | } 42 | }), 43 | ); 44 | } 45 | 46 | getFeed() async { 47 | List items = []; 48 | var snap = await FirebaseFirestore.instance 49 | .collection('insta_a_feed') 50 | .doc(currentUserModel.id) 51 | .collection("items") 52 | .orderBy("timestamp") 53 | .get(); 54 | 55 | for (var doc in snap.docs) { 56 | items.add(ActivityFeedItem.fromDocument(doc)); 57 | } 58 | return items; 59 | } 60 | 61 | // ensures state is kept when switching pages 62 | @override 63 | bool get wantKeepAlive => true; 64 | 65 | } 66 | 67 | class ActivityFeedItem extends StatelessWidget { 68 | final String username; 69 | final String userId; 70 | final String 71 | type; // types include liked photo, follow user, comment on photo 72 | final String mediaUrl; 73 | final String mediaId; 74 | final String userProfileImg; 75 | final String commentData; 76 | 77 | ActivityFeedItem( 78 | {this.username, 79 | this.userId, 80 | this.type, 81 | this.mediaUrl, 82 | this.mediaId, 83 | this.userProfileImg, 84 | this.commentData}); 85 | 86 | factory ActivityFeedItem.fromDocument(DocumentSnapshot document) { 87 | var data = document.data(); 88 | return ActivityFeedItem( 89 | username: data['username'], 90 | userId: data['userId'], 91 | type: data['type'], 92 | mediaUrl: data['mediaUrl'], 93 | mediaId: data['postId'], 94 | userProfileImg: data['userProfileImg'], 95 | commentData: data["commentData"], 96 | ); 97 | } 98 | 99 | Widget mediaPreview = Container(); 100 | String actionText; 101 | 102 | void configureItem(BuildContext context) { 103 | if (type == "like" || type == "comment") { 104 | mediaPreview = GestureDetector( 105 | onTap: () { 106 | openImage(context, mediaId); 107 | }, 108 | child: Container( 109 | height: 45.0, 110 | width: 45.0, 111 | child: AspectRatio( 112 | aspectRatio: 487 / 451, 113 | child: Container( 114 | decoration: BoxDecoration( 115 | image: DecorationImage( 116 | fit: BoxFit.fill, 117 | alignment: FractionalOffset.topCenter, 118 | image: NetworkImage(mediaUrl), 119 | )), 120 | ), 121 | ), 122 | ), 123 | ); 124 | } 125 | 126 | if (type == "like") { 127 | actionText = " liked your post."; 128 | } else if (type == "follow") { 129 | actionText = " starting following you."; 130 | } else if (type == "comment") { 131 | actionText = " commented: $commentData"; 132 | } else { 133 | actionText = "Error - invalid activityFeed type: $type"; 134 | } 135 | } 136 | 137 | @override 138 | Widget build(BuildContext context) { 139 | configureItem(context); 140 | return Row( 141 | mainAxisSize: MainAxisSize.max, 142 | children: [ 143 | Padding( 144 | padding: const EdgeInsets.only(left: 20.0, right: 15.0), 145 | child: CircleAvatar( 146 | radius: 23.0, 147 | backgroundImage: NetworkImage(userProfileImg), 148 | ), 149 | ), 150 | Expanded( 151 | child: Row( 152 | mainAxisSize: MainAxisSize.min, 153 | children: [ 154 | GestureDetector( 155 | child: Text( 156 | username, 157 | style: TextStyle(fontWeight: FontWeight.bold), 158 | ), 159 | onTap: () { 160 | openProfile(context, userId); 161 | }, 162 | ), 163 | Flexible( 164 | child: Container( 165 | child: Text( 166 | actionText, 167 | overflow: TextOverflow.ellipsis, 168 | ), 169 | ), 170 | ) 171 | ], 172 | ), 173 | ), 174 | Container( 175 | child: Align( 176 | child: Padding( 177 | child: mediaPreview, 178 | padding: EdgeInsets.all(15.0), 179 | ), 180 | alignment: AlignmentDirectional.bottomEnd)) 181 | ], 182 | ); 183 | } 184 | } 185 | 186 | openImage(BuildContext context, String imageId) { 187 | print("the image id is $imageId"); 188 | Navigator.of(context) 189 | .push(MaterialPageRoute(builder: (BuildContext context) { 190 | return Center( 191 | child: Scaffold( 192 | appBar: AppBar( 193 | title: Text('Photo', 194 | style: TextStyle( 195 | color: Colors.black, fontWeight: FontWeight.bold)), 196 | backgroundColor: Colors.white, 197 | ), 198 | body: ListView( 199 | children: [ 200 | Container( 201 | child: ImagePostFromId(id: imageId), 202 | ), 203 | ], 204 | )), 205 | ); 206 | })); 207 | } 208 | -------------------------------------------------------------------------------- /lib/comment_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import "dart:async"; 4 | import "main.dart"; //for current user 5 | 6 | class CommentScreen extends StatefulWidget { 7 | final String postId; 8 | final String postOwner; 9 | final String postMediaUrl; 10 | 11 | const CommentScreen({this.postId, this.postOwner, this.postMediaUrl}); 12 | @override 13 | _CommentScreenState createState() => _CommentScreenState( 14 | postId: this.postId, 15 | postOwner: this.postOwner, 16 | postMediaUrl: this.postMediaUrl); 17 | } 18 | 19 | class _CommentScreenState extends State { 20 | final String postId; 21 | final String postOwner; 22 | final String postMediaUrl; 23 | 24 | bool didFetchComments = false; 25 | List fetchedComments = []; 26 | 27 | final TextEditingController _commentController = TextEditingController(); 28 | 29 | _CommentScreenState({this.postId, this.postOwner, this.postMediaUrl}); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | appBar: AppBar( 35 | title: Text( 36 | "Comments", 37 | style: TextStyle(color: Colors.black), 38 | ), 39 | backgroundColor: Colors.white, 40 | ), 41 | body: buildPage(), 42 | ); 43 | } 44 | 45 | Widget buildPage() { 46 | return Column( 47 | children: [ 48 | Expanded( 49 | child: 50 | buildComments(), 51 | ), 52 | Divider(), 53 | ListTile( 54 | title: TextFormField( 55 | controller: _commentController, 56 | decoration: InputDecoration(labelText: 'Write a comment...'), 57 | onFieldSubmitted: addComment, 58 | ), 59 | trailing: OutlineButton(onPressed: (){addComment(_commentController.text);}, borderSide: BorderSide.none, child: Text("Post"),), 60 | ), 61 | ], 62 | ); 63 | } 64 | 65 | 66 | Widget buildComments() { 67 | if (this.didFetchComments == false){ 68 | return FutureBuilder>( 69 | future: getComments(), 70 | builder: (context, snapshot) { 71 | if (!snapshot.hasData) 72 | return Container( 73 | alignment: FractionalOffset.center, 74 | child: CircularProgressIndicator()); 75 | 76 | this.didFetchComments = true; 77 | this.fetchedComments = snapshot.data; 78 | return ListView( 79 | children: snapshot.data, 80 | ); 81 | }); 82 | } else { 83 | // for optimistic updating 84 | return ListView( 85 | children: this.fetchedComments 86 | ); 87 | } 88 | } 89 | 90 | Future> getComments() async { 91 | List comments = []; 92 | 93 | QuerySnapshot data = await FirebaseFirestore.instance 94 | .collection("insta_comments") 95 | .doc(postId) 96 | .collection("comments") 97 | .get(); 98 | data.docs.forEach((DocumentSnapshot doc) { 99 | comments.add(Comment.fromDocument(doc)); 100 | }); 101 | return comments; 102 | } 103 | 104 | addComment(String comment) { 105 | _commentController.clear(); 106 | FirebaseFirestore.instance 107 | .collection("insta_comments") 108 | .doc(postId) 109 | .collection("comments") 110 | .add({ 111 | "username": currentUserModel.username, 112 | "comment": comment, 113 | "timestamp": Timestamp.now(), 114 | "avatarUrl": currentUserModel.photoUrl, 115 | "userId": currentUserModel.id 116 | }); 117 | 118 | //adds to postOwner's activity feed 119 | FirebaseFirestore.instance 120 | .collection("insta_a_feed") 121 | .doc(postOwner) 122 | .collection("items") 123 | .add({ 124 | "username": currentUserModel.username, 125 | "userId": currentUserModel.id, 126 | "type": "comment", 127 | "userProfileImg": currentUserModel.photoUrl, 128 | "commentData": comment, 129 | "timestamp": Timestamp.now(), 130 | "postId": postId, 131 | "mediaUrl": postMediaUrl, 132 | }); 133 | 134 | // add comment to the current listview for an optimistic update 135 | setState(() { 136 | fetchedComments = List.from(fetchedComments)..add(Comment( 137 | username: currentUserModel.username, 138 | comment: comment, 139 | timestamp: Timestamp.now(), 140 | avatarUrl: currentUserModel.photoUrl, 141 | userId: currentUserModel.id 142 | )); 143 | }); 144 | } 145 | } 146 | 147 | class Comment extends StatelessWidget { 148 | final String username; 149 | final String userId; 150 | final String avatarUrl; 151 | final String comment; 152 | final Timestamp timestamp; 153 | 154 | Comment( 155 | {this.username, 156 | this.userId, 157 | this.avatarUrl, 158 | this.comment, 159 | this.timestamp}); 160 | 161 | factory Comment.fromDocument(DocumentSnapshot document) { 162 | var data = document.data(); 163 | return Comment( 164 | username: data['username'], 165 | userId: data['userId'], 166 | comment: data["comment"], 167 | timestamp: data["timestamp"], 168 | avatarUrl: data["avatarUrl"], 169 | ); 170 | } 171 | 172 | @override 173 | Widget build(BuildContext context) { 174 | return Column( 175 | children: [ 176 | ListTile( 177 | title: Text(comment), 178 | leading: CircleAvatar( 179 | backgroundImage: NetworkImage(avatarUrl), 180 | ), 181 | ), 182 | Divider(), 183 | ], 184 | ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lib/create_account.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CreateAccount extends StatefulWidget { 4 | @override 5 | _CreateAccountState createState() => _CreateAccountState(); 6 | } 7 | 8 | class _CreateAccountState extends State { 9 | final name = TextEditingController(); 10 | 11 | 12 | @override 13 | void dispose() { 14 | // Clean up the controller when the Widget is removed from the Widget tree 15 | name.dispose(); 16 | super.dispose(); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context2) { 21 | return Column(children: [ 22 | Padding( 23 | padding: const EdgeInsets.only(top: 25.0), 24 | child: Center( 25 | child: Text( 26 | "Create a username", 27 | style: TextStyle(fontSize: 25.0), 28 | ), 29 | ), 30 | ), 31 | Padding( 32 | padding: const EdgeInsets.all(16.0), 33 | child: Container( 34 | decoration: BoxDecoration( 35 | border: Border.all(width: 1.0, color: Colors.black26), 36 | borderRadius: BorderRadius.circular(7.0)), 37 | child: TextField( 38 | controller: name, 39 | decoration: InputDecoration( 40 | border: InputBorder.none, 41 | contentPadding: const EdgeInsets.all(10.0), 42 | labelText: "Username", 43 | labelStyle: TextStyle(fontSize: 15.0)), 44 | ), 45 | ), 46 | ), 47 | GestureDetector( 48 | onTap: () { 49 | if (name.text == null || name.text.length == 0){ 50 | return; 51 | } 52 | Navigator.pop(context, name.text); 53 | }, 54 | 55 | 56 | child: Container( 57 | width: 350.0, 58 | height: 50.0, 59 | child: Center( 60 | child: Text( 61 | "Next", 62 | style: TextStyle( 63 | color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.bold), 64 | )), 65 | decoration: BoxDecoration( 66 | color: Colors.blue, borderRadius: BorderRadius.circular(7.0)), 67 | )) 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/edit_profile_page.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'main.dart'; //for currentuser & google signin instance 4 | import 'models/user.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | class EditProfilePage extends StatelessWidget { 8 | final TextEditingController nameController = TextEditingController(); 9 | final TextEditingController bioController = TextEditingController(); 10 | 11 | changeProfilePhoto(BuildContext parentContext) { 12 | return showDialog( 13 | context: parentContext, 14 | builder: (BuildContext context) { 15 | return AlertDialog( 16 | title: Text('Change Photo'), 17 | content: SingleChildScrollView( 18 | child: ListBody( 19 | children: [ 20 | Text( 21 | 'Changing your profile photo has not been implemented yet'), 22 | ], 23 | ), 24 | ), 25 | ); 26 | }, 27 | ); 28 | } 29 | 30 | applyChanges() { 31 | FirebaseFirestore.instance 32 | .collection('insta_users') 33 | .doc(currentUserModel.id) 34 | .update({ 35 | "displayName": nameController.text, 36 | "bio": bioController.text, 37 | }); 38 | } 39 | 40 | Widget buildTextField({String name, TextEditingController controller}) { 41 | return Column( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | Padding( 45 | padding: const EdgeInsets.only(top: 12.0), 46 | child: Text( 47 | name, 48 | style: TextStyle(color: Colors.grey), 49 | ), 50 | ), 51 | TextField( 52 | controller: controller, 53 | decoration: InputDecoration( 54 | hintText: name, 55 | ), 56 | ), 57 | ], 58 | ); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return FutureBuilder( 64 | future: FirebaseFirestore.instance 65 | .collection('insta_users') 66 | .doc(currentUserModel.id) 67 | .get(), 68 | builder: (context, snapshot) { 69 | if (!snapshot.hasData) 70 | return Container( 71 | alignment: FractionalOffset.center, 72 | child: CircularProgressIndicator()); 73 | 74 | User user = User.fromDocument(snapshot.data); 75 | 76 | nameController.text = user.displayName; 77 | bioController.text = user.bio; 78 | 79 | return Column( 80 | children: [ 81 | Padding( 82 | padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), 83 | child: CircleAvatar( 84 | backgroundImage: NetworkImage(currentUserModel.photoUrl), 85 | radius: 50.0, 86 | ), 87 | ), 88 | FlatButton( 89 | onPressed: () { 90 | changeProfilePhoto(context); 91 | }, 92 | child: Text( 93 | "Change Photo", 94 | style: const TextStyle( 95 | color: Colors.blue, 96 | fontSize: 20.0, 97 | fontWeight: FontWeight.bold), 98 | )), 99 | Padding( 100 | padding: const EdgeInsets.all(16.0), 101 | child: Column( 102 | children: [ 103 | buildTextField(name: "Name", controller: nameController), 104 | buildTextField(name: "Bio", controller: bioController), 105 | ], 106 | ), 107 | ), 108 | Padding( 109 | padding: const EdgeInsets.all(16.0), 110 | child: MaterialButton( 111 | onPressed: () => {_logout(context)}, 112 | child: Text("Logout") 113 | 114 | ) 115 | ) 116 | ], 117 | ); 118 | }); 119 | } 120 | 121 | void _logout(BuildContext context) async { 122 | print("logout"); 123 | await auth.signOut(); 124 | await googleSignIn.signOut(); 125 | 126 | SharedPreferences prefs = await SharedPreferences.getInstance(); 127 | await prefs.clear(); 128 | 129 | currentUserModel = null; 130 | 131 | Navigator.pop(context); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/feed.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'image_post.dart'; 3 | import 'dart:async'; 4 | import 'main.dart'; 5 | import 'dart:io'; 6 | import 'dart:convert'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | class Feed extends StatefulWidget { 10 | _Feed createState() => _Feed(); 11 | } 12 | 13 | class _Feed extends State with AutomaticKeepAliveClientMixin { 14 | List feedData; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | this._loadFeed(); 20 | } 21 | 22 | buildFeed() { 23 | if (feedData != null) { 24 | return ListView( 25 | children: feedData, 26 | ); 27 | } else { 28 | return Container( 29 | alignment: FractionalOffset.center, 30 | child: CircularProgressIndicator()); 31 | } 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | super.build(context); // reloads state when opened again 37 | 38 | return Scaffold( 39 | appBar: AppBar( 40 | title: const Text('Fluttergram', 41 | style: const TextStyle( 42 | fontFamily: "Billabong", color: Colors.black, fontSize: 35.0)), 43 | centerTitle: true, 44 | backgroundColor: Colors.white, 45 | ), 46 | body: RefreshIndicator( 47 | onRefresh: _refresh, 48 | child: buildFeed(), 49 | ), 50 | ); 51 | } 52 | 53 | Future _refresh() async { 54 | await _getFeed(); 55 | 56 | setState(() {}); 57 | 58 | return; 59 | } 60 | 61 | _loadFeed() async { 62 | SharedPreferences prefs = await SharedPreferences.getInstance(); 63 | String json = prefs.getString("feed"); 64 | 65 | if (json != null) { 66 | List> data = 67 | jsonDecode(json).cast>(); 68 | List listOfPosts = _generateFeed(data); 69 | setState(() { 70 | feedData = listOfPosts; 71 | }); 72 | } else { 73 | _getFeed(); 74 | } 75 | } 76 | 77 | _getFeed() async { 78 | print("Staring getFeed"); 79 | 80 | SharedPreferences prefs = await SharedPreferences.getInstance(); 81 | 82 | String userId = googleSignIn.currentUser.id.toString(); 83 | var url = 84 | 'https://us-central1-mp-rps.cloudfunctions.net/getFeed?uid=' + userId; 85 | var httpClient = HttpClient(); 86 | 87 | List listOfPosts; 88 | String result; 89 | try { 90 | var request = await httpClient.getUrl(Uri.parse(url)); 91 | var response = await request.close(); 92 | if (response.statusCode == HttpStatus.ok) { 93 | String json = await response.transform(utf8.decoder).join(); 94 | prefs.setString("feed", json); 95 | List> data = 96 | jsonDecode(json).cast>(); 97 | listOfPosts = _generateFeed(data); 98 | result = "Success in http request for feed"; 99 | } else { 100 | result = 101 | 'Error getting a feed: Http status ${response.statusCode} | userId $userId'; 102 | } 103 | } catch (exception) { 104 | result = 'Failed invoking the getFeed function. Exception: $exception'; 105 | } 106 | print(result); 107 | 108 | setState(() { 109 | feedData = listOfPosts; 110 | }); 111 | } 112 | 113 | List _generateFeed(List> feedData) { 114 | List listOfPosts = []; 115 | 116 | for (var postData in feedData) { 117 | listOfPosts.add(ImagePost.fromJSON(postData)); 118 | } 119 | 120 | return listOfPosts; 121 | } 122 | 123 | // ensures state is kept when switching pages 124 | @override 125 | bool get wantKeepAlive => true; 126 | } 127 | -------------------------------------------------------------------------------- /lib/image_post.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 | import 'main.dart'; 5 | import 'dart:async'; 6 | import 'profile_page.dart'; 7 | import 'package:cached_network_image/cached_network_image.dart'; 8 | import 'comment_screen.dart'; 9 | import 'package:flare_flutter/flare_actor.dart'; 10 | 11 | class ImagePost extends StatefulWidget { 12 | const ImagePost( 13 | {this.mediaUrl, 14 | this.username, 15 | this.location, 16 | this.description, 17 | this.likes, 18 | this.postId, 19 | this.ownerId}); 20 | 21 | factory ImagePost.fromDocument(DocumentSnapshot document) { 22 | return ImagePost( 23 | username: document['username'], 24 | location: document['location'], 25 | mediaUrl: document['mediaUrl'], 26 | likes: document['likes'], 27 | description: document['description'], 28 | postId: document.id, 29 | ownerId: document['ownerId'], 30 | ); 31 | } 32 | 33 | factory ImagePost.fromJSON(Map data) { 34 | return ImagePost( 35 | username: data['username'], 36 | location: data['location'], 37 | mediaUrl: data['mediaUrl'], 38 | likes: data['likes'], 39 | description: data['description'], 40 | ownerId: data['ownerId'], 41 | postId: data['postId'], 42 | ); 43 | } 44 | 45 | int getLikeCount(var likes) { 46 | if (likes == null) { 47 | return 0; 48 | } 49 | // issue is below 50 | var vals = likes.values; 51 | int count = 0; 52 | for (var val in vals) { 53 | if (val == true) { 54 | count = count + 1; 55 | } 56 | } 57 | 58 | return count; 59 | } 60 | 61 | final String mediaUrl; 62 | final String username; 63 | final String location; 64 | final String description; 65 | final likes; 66 | final String postId; 67 | final String ownerId; 68 | 69 | _ImagePost createState() => _ImagePost( 70 | mediaUrl: this.mediaUrl, 71 | username: this.username, 72 | location: this.location, 73 | description: this.description, 74 | likes: this.likes, 75 | likeCount: getLikeCount(this.likes), 76 | ownerId: this.ownerId, 77 | postId: this.postId, 78 | ); 79 | } 80 | 81 | class _ImagePost extends State { 82 | final String mediaUrl; 83 | final String username; 84 | final String location; 85 | final String description; 86 | Map likes; 87 | int likeCount; 88 | final String postId; 89 | bool liked; 90 | final String ownerId; 91 | 92 | bool showHeart = false; 93 | 94 | TextStyle boldStyle = TextStyle( 95 | color: Colors.black, 96 | fontWeight: FontWeight.bold, 97 | ); 98 | 99 | var reference = FirebaseFirestore.instance.collection('insta_posts'); 100 | 101 | _ImagePost( 102 | {this.mediaUrl, 103 | this.username, 104 | this.location, 105 | this.description, 106 | this.likes, 107 | this.postId, 108 | this.likeCount, 109 | this.ownerId}); 110 | 111 | GestureDetector buildLikeIcon() { 112 | Color color; 113 | IconData icon; 114 | 115 | if (liked) { 116 | color = Colors.pink; 117 | icon = FontAwesomeIcons.solidHeart; 118 | } else { 119 | icon = FontAwesomeIcons.heart; 120 | } 121 | 122 | return GestureDetector( 123 | child: Icon( 124 | icon, 125 | size: 25.0, 126 | color: color, 127 | ), 128 | onTap: () { 129 | _likePost(postId); 130 | }); 131 | } 132 | 133 | GestureDetector buildLikeableImage() { 134 | return GestureDetector( 135 | onDoubleTap: () => _likePost(postId), 136 | child: Stack( 137 | alignment: Alignment.center, 138 | children: [ 139 | CachedNetworkImage( 140 | imageUrl: mediaUrl, 141 | fit: BoxFit.fitWidth, 142 | placeholder: (context, url) => loadingPlaceHolder, 143 | errorWidget: (context, url, error) => Icon(Icons.error), 144 | ), 145 | showHeart 146 | ? Positioned( 147 | child: Container( 148 | width: 100, 149 | height: 100, 150 | child: Opacity( 151 | opacity: 0.85, 152 | child: FlareActor("assets/flare/Like.flr", 153 | animation: "Like", 154 | )), 155 | ), 156 | ) 157 | : Container() 158 | ], 159 | ), 160 | ); 161 | } 162 | 163 | buildPostHeader({String ownerId}) { 164 | if (ownerId == null) { 165 | return Text("owner error"); 166 | } 167 | 168 | return FutureBuilder( 169 | future: FirebaseFirestore.instance 170 | .collection('insta_users') 171 | .doc(ownerId) 172 | .get(), 173 | builder: (context, snapshot) { 174 | 175 | if (snapshot.data != null) { 176 | return ListTile( 177 | leading: CircleAvatar( 178 | backgroundImage: CachedNetworkImageProvider(snapshot.data.data()['photoUrl']), 179 | backgroundColor: Colors.grey, 180 | ), 181 | title: GestureDetector( 182 | child: Text(snapshot.data.data()['username'], style: boldStyle), 183 | onTap: () { 184 | openProfile(context, ownerId); 185 | }, 186 | ), 187 | subtitle: Text(this.location), 188 | trailing: const Icon(Icons.more_vert), 189 | ); 190 | } 191 | 192 | // snapshot data is null here 193 | return Container(); 194 | }); 195 | } 196 | 197 | Container loadingPlaceHolder = Container( 198 | height: 400.0, 199 | child: Center(child: CircularProgressIndicator()), 200 | ); 201 | 202 | @override 203 | Widget build(BuildContext context) { 204 | liked = (likes[googleSignIn.currentUser.id.toString()] == true); 205 | 206 | return Column( 207 | mainAxisSize: MainAxisSize.min, 208 | children: [ 209 | buildPostHeader(ownerId: ownerId), 210 | buildLikeableImage(), 211 | Row( 212 | mainAxisAlignment: MainAxisAlignment.start, 213 | children: [ 214 | Padding(padding: const EdgeInsets.only(left: 20.0, top: 40.0)), 215 | buildLikeIcon(), 216 | Padding(padding: const EdgeInsets.only(right: 20.0)), 217 | GestureDetector( 218 | child: const Icon( 219 | FontAwesomeIcons.comment, 220 | size: 25.0, 221 | ), 222 | onTap: () { 223 | goToComments( 224 | context: context, 225 | postId: postId, 226 | ownerId: ownerId, 227 | mediaUrl: mediaUrl); 228 | }), 229 | ], 230 | ), 231 | Row( 232 | children: [ 233 | Container( 234 | margin: const EdgeInsets.only(left: 20.0), 235 | child: Text( 236 | "$likeCount likes", 237 | style: boldStyle, 238 | ), 239 | ) 240 | ], 241 | ), 242 | Row( 243 | crossAxisAlignment: CrossAxisAlignment.start, 244 | children: [ 245 | Container( 246 | margin: const EdgeInsets.only(left: 20.0), 247 | child: Text( 248 | "$username ", 249 | style: boldStyle, 250 | )), 251 | Expanded(child: Text(description)), 252 | ], 253 | ) 254 | ], 255 | ); 256 | } 257 | 258 | void _likePost(String postId2) { 259 | var userId = googleSignIn.currentUser.id; 260 | bool _liked = likes[userId] == true; 261 | 262 | if (_liked) { 263 | print('removing like'); 264 | reference.doc(postId).update({ 265 | 'likes.$userId': false 266 | //firestore plugin doesnt support deleting, so it must be nulled / falsed 267 | }); 268 | 269 | setState(() { 270 | likeCount = likeCount - 1; 271 | liked = false; 272 | likes[userId] = false; 273 | }); 274 | 275 | removeActivityFeedItem(); 276 | } 277 | 278 | if (!_liked) { 279 | print('liking'); 280 | reference.doc(postId).update({'likes.$userId': true}); 281 | 282 | addActivityFeedItem(); 283 | 284 | setState(() { 285 | likeCount = likeCount + 1; 286 | liked = true; 287 | likes[userId] = true; 288 | showHeart = true; 289 | }); 290 | Timer(const Duration(milliseconds: 2000), () { 291 | setState(() { 292 | showHeart = false; 293 | }); 294 | }); 295 | } 296 | } 297 | 298 | void addActivityFeedItem() { 299 | FirebaseFirestore.instance 300 | .collection("insta_a_feed") 301 | .doc(ownerId) 302 | .collection("items") 303 | .doc(postId) 304 | .set({ 305 | "username": currentUserModel.username, 306 | "userId": currentUserModel.id, 307 | "type": "like", 308 | "userProfileImg": currentUserModel.photoUrl, 309 | "mediaUrl": mediaUrl, 310 | "timestamp": DateTime.now(), 311 | "postId": postId, 312 | }); 313 | } 314 | 315 | void removeActivityFeedItem() { 316 | FirebaseFirestore.instance 317 | .collection("insta_a_feed") 318 | .doc(ownerId) 319 | .collection("items") 320 | .doc(postId) 321 | .delete(); 322 | } 323 | } 324 | 325 | class ImagePostFromId extends StatelessWidget { 326 | final String id; 327 | 328 | const ImagePostFromId({this.id}); 329 | 330 | getImagePost() async { 331 | var document = 332 | await FirebaseFirestore.instance.collection('insta_posts').doc(id).get(); 333 | return ImagePost.fromDocument(document); 334 | } 335 | 336 | @override 337 | Widget build(BuildContext context) { 338 | return FutureBuilder( 339 | future: getImagePost(), 340 | builder: (context, snapshot) { 341 | if (!snapshot.hasData) 342 | return Container( 343 | alignment: FractionalOffset.center, 344 | padding: const EdgeInsets.only(top: 10.0), 345 | child: CircularProgressIndicator()); 346 | return snapshot.data; 347 | }); 348 | } 349 | } 350 | 351 | void goToComments( 352 | {BuildContext context, String postId, String ownerId, String mediaUrl}) { 353 | Navigator.of(context) 354 | .push(MaterialPageRoute(builder: (BuildContext context) { 355 | return CommentScreen( 356 | postId: postId, 357 | postOwner: ownerId, 358 | postMediaUrl: mediaUrl, 359 | ); 360 | })); 361 | } 362 | -------------------------------------------------------------------------------- /lib/location.dart: -------------------------------------------------------------------------------- 1 | /* File to get location of user 2 | * used dependencies - location => to get location coordinates of user, 3 | * - geoLocation => To get Address from the location coordinates 4 | */ 5 | import 'package:flutter/services.dart'; 6 | import 'package:geocoder/geocoder.dart'; 7 | import 'package:location/location.dart'; 8 | 9 | getUserLocation() async { 10 | LocationData currentLocation; 11 | String error; 12 | Location location = Location(); 13 | try { 14 | currentLocation = await location.getLocation(); 15 | } on PlatformException catch (e) { 16 | if (e.code == 'PERMISSION_DENIED') { 17 | error = 'please grant permission'; 18 | print(error); 19 | } 20 | if (e.code == 'PERMISSION_DENIED_NEVER_ASK') { 21 | error = 'permission denied- please enable it from app settings'; 22 | print(error); 23 | } 24 | currentLocation = null; 25 | } 26 | final coordinates = Coordinates( 27 | currentLocation.latitude, currentLocation.longitude); 28 | var addresses = 29 | await Geocoder.local.findAddressesFromCoordinates(coordinates); 30 | var first = addresses.first; 31 | return first; 32 | } 33 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'feed.dart'; 4 | import 'upload_page.dart'; 5 | import 'dart:async'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:google_sign_in/google_sign_in.dart'; 8 | import 'package:firebase_auth/firebase_auth.dart' as FBA; 9 | import 'package:cloud_firestore/cloud_firestore.dart'; 10 | import 'profile_page.dart'; 11 | import 'search_page.dart'; 12 | import 'activity_feed.dart'; 13 | import 'create_account.dart'; 14 | import 'package:firebase_messaging/firebase_messaging.dart'; 15 | import 'dart:io' show Platform; 16 | import 'models/user.dart'; 17 | 18 | final auth = FBA.FirebaseAuth.instance; 19 | final googleSignIn = GoogleSignIn(); 20 | final ref = FirebaseFirestore.instance.collection('insta_users'); 21 | final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; 22 | 23 | 24 | User currentUserModel; 25 | 26 | Future main() async { 27 | 28 | WidgetsFlutterBinding.ensureInitialized(); // after upgrading flutter this is now necessary 29 | 30 | runApp(Fluttergram()); 31 | } 32 | 33 | Future _ensureLoggedIn(BuildContext context) async { 34 | GoogleSignInAccount user = googleSignIn.currentUser; 35 | if (user == null) { 36 | user = await googleSignIn.signInSilently(); 37 | } 38 | if (user == null) { 39 | await googleSignIn.signIn(); 40 | await tryCreateUserRecord(context); 41 | } 42 | 43 | if (auth.currentUser == null) { 44 | 45 | final GoogleSignInAccount googleUser = await googleSignIn.signIn(); 46 | final GoogleSignInAuthentication googleAuth = await googleUser 47 | .authentication; 48 | 49 | final FBA.GoogleAuthCredential credential = FBA.GoogleAuthProvider.credential( 50 | accessToken: googleAuth.accessToken, 51 | idToken: googleAuth.idToken, 52 | ); 53 | 54 | await auth.signInWithCredential(credential); 55 | } 56 | } 57 | 58 | Future _silentLogin(BuildContext context) async { 59 | GoogleSignInAccount user = googleSignIn.currentUser; 60 | 61 | if (user == null) { 62 | user = await googleSignIn.signInSilently(); 63 | await tryCreateUserRecord(context); 64 | } 65 | 66 | if (await auth.currentUser == null && user != null) { 67 | final GoogleSignInAccount googleUser = await googleSignIn.signIn(); 68 | final GoogleSignInAuthentication googleAuth = await googleUser 69 | .authentication; 70 | 71 | 72 | final FBA.GoogleAuthCredential credential = FBA.GoogleAuthProvider.credential( 73 | accessToken: googleAuth.accessToken, 74 | idToken: googleAuth.idToken, 75 | ); 76 | 77 | await auth.signInWithCredential(credential); 78 | } 79 | 80 | 81 | } 82 | 83 | Future _setUpNotifications() async { 84 | if (Platform.isAndroid) { 85 | 86 | _firebaseMessaging.getToken().then((token) { 87 | print("Firebase Messaging Token: " + token); 88 | 89 | FirebaseFirestore.instance 90 | .collection("insta_users") 91 | .doc(currentUserModel.id) 92 | .update({"androidNotificationToken": token}); 93 | }); 94 | } 95 | } 96 | 97 | Future tryCreateUserRecord(BuildContext context) async { 98 | GoogleSignInAccount user = googleSignIn.currentUser; 99 | if (user == null) { 100 | return null; 101 | } 102 | DocumentSnapshot userRecord = await ref.doc(user.id).get(); 103 | if (userRecord.data() == null) { 104 | // no user record exists, time to create 105 | 106 | String userName = await Navigator.push( 107 | context, 108 | MaterialPageRoute( 109 | builder: (context) => Center( 110 | child: Scaffold( 111 | appBar: AppBar( 112 | leading: Container(), 113 | title: Text('Fill out missing data', 114 | style: TextStyle( 115 | color: Colors.black, 116 | fontWeight: FontWeight.bold)), 117 | backgroundColor: Colors.white, 118 | ), 119 | body: ListView( 120 | children: [ 121 | Container( 122 | child: CreateAccount(), 123 | ), 124 | ], 125 | )), 126 | )), 127 | ); 128 | 129 | if (userName != null || userName.length != 0) { 130 | ref.doc(user.id).set({ 131 | "id": user.id, 132 | "username": userName, 133 | "photoUrl": user.photoUrl, 134 | "email": user.email, 135 | "displayName": user.displayName, 136 | "bio": "", 137 | "followers": {}, 138 | "following": {}, 139 | }); 140 | } 141 | userRecord = await ref.doc(user.id).get(); 142 | } 143 | 144 | currentUserModel = User.fromDocument(userRecord); 145 | return null; 146 | } 147 | 148 | class Fluttergram extends StatelessWidget { 149 | // This widget is the root of your application. 150 | @override 151 | Widget build(BuildContext context) { 152 | return MaterialApp( 153 | title: 'Fluttergram', 154 | theme: ThemeData( 155 | // This is the theme of your application. 156 | // 157 | // Try running your application with "flutter run". You'll see the 158 | // application has a blue toolbar. Then, without quitting the app, try 159 | // changing the primarySwatch below to Colors.green and then invoke 160 | // "hot reload" (press "r" in the console where you ran "flutter run", 161 | // or press Run > Flutter Hot Reload in IntelliJ). Notice that the 162 | // counter didn't reset back to zero; the application is not restarted. 163 | primarySwatch: Colors.blue, 164 | buttonColor: Colors.pink, 165 | primaryIconTheme: IconThemeData(color: Colors.black)), 166 | home: HomePage(title: 'Fluttergram'), 167 | ); 168 | } 169 | } 170 | 171 | class HomePage extends StatefulWidget { 172 | HomePage({Key key, this.title}) : super(key: key); 173 | final String title; 174 | 175 | @override 176 | _HomePageState createState() => _HomePageState(); 177 | } 178 | 179 | PageController pageController; 180 | 181 | class _HomePageState extends State { 182 | int _page = 0; 183 | bool triedSilentLogin = false; 184 | bool setupNotifications = false; 185 | bool firebaseInitialized = false; 186 | 187 | Scaffold buildLoginPage() { 188 | return Scaffold( 189 | body: Center( 190 | child: Padding( 191 | padding: const EdgeInsets.only(top: 240.0), 192 | child: Column( 193 | children: [ 194 | Text( 195 | 'Fluttergram', 196 | style: TextStyle( 197 | fontSize: 60.0, 198 | fontFamily: "Billabong", 199 | color: Colors.black), 200 | ), 201 | Padding(padding: const EdgeInsets.only(bottom: 100.0)), 202 | GestureDetector( 203 | onTap: login, 204 | child: Image.asset( 205 | "assets/images/google_signin_button.png", 206 | width: 225.0, 207 | ), 208 | ) 209 | ], 210 | ), 211 | ), 212 | ), 213 | ); 214 | } 215 | 216 | @override 217 | Widget build(BuildContext context) { 218 | if (triedSilentLogin == false) { 219 | silentLogin(context); 220 | } 221 | 222 | if (setupNotifications == false && currentUserModel != null) { 223 | setUpNotifications(); 224 | } 225 | 226 | if (!firebaseInitialized) return CircularProgressIndicator(); 227 | 228 | auth.authStateChanges().listen((event) { 229 | if (event == null) { 230 | silentLogin(context); 231 | } 232 | }); 233 | 234 | return (googleSignIn.currentUser == null || currentUserModel == null) 235 | ? buildLoginPage() 236 | : Scaffold( 237 | body: PageView( 238 | children: [ 239 | Container( 240 | color: Colors.white, 241 | child: Feed(), 242 | ), 243 | Container(color: Colors.white, child: SearchPage()), 244 | Container( 245 | color: Colors.white, 246 | child: Uploader(), 247 | ), 248 | Container( 249 | color: Colors.white, child: ActivityFeedPage()), 250 | Container( 251 | color: Colors.white, 252 | child: ProfilePage( 253 | userId: googleSignIn.currentUser.id, 254 | )), 255 | ], 256 | controller: pageController, 257 | physics: NeverScrollableScrollPhysics(), 258 | onPageChanged: onPageChanged, 259 | ), 260 | bottomNavigationBar: CupertinoTabBar( 261 | backgroundColor: Colors.white, 262 | items: [ 263 | BottomNavigationBarItem( 264 | icon: Icon(Icons.home, 265 | color: (_page == 0) ? Colors.black : Colors.grey), 266 | title: Container(height: 0.0), 267 | backgroundColor: Colors.white), 268 | BottomNavigationBarItem( 269 | icon: Icon(Icons.search, 270 | color: (_page == 1) ? Colors.black : Colors.grey), 271 | title: Container(height: 0.0), 272 | backgroundColor: Colors.white), 273 | BottomNavigationBarItem( 274 | icon: Icon(Icons.add_circle, 275 | color: (_page == 2) ? Colors.black : Colors.grey), 276 | title: Container(height: 0.0), 277 | backgroundColor: Colors.white), 278 | BottomNavigationBarItem( 279 | icon: Icon(Icons.star, 280 | color: (_page == 3) ? Colors.black : Colors.grey), 281 | title: Container(height: 0.0), 282 | backgroundColor: Colors.white), 283 | BottomNavigationBarItem( 284 | icon: Icon(Icons.person, 285 | color: (_page == 4) ? Colors.black : Colors.grey), 286 | title: Container(height: 0.0), 287 | backgroundColor: Colors.white), 288 | ], 289 | onTap: navigationTapped, 290 | currentIndex: _page, 291 | ), 292 | ); 293 | } 294 | 295 | void login() async { 296 | await _ensureLoggedIn(context); 297 | setState(() { 298 | triedSilentLogin = true; 299 | }); 300 | } 301 | 302 | void setUpNotifications() { 303 | _setUpNotifications(); 304 | setState(() { 305 | setupNotifications = true; 306 | }); 307 | } 308 | 309 | void silentLogin(BuildContext context) async { 310 | await _silentLogin(context); 311 | setState(() { 312 | triedSilentLogin = true; 313 | }); 314 | } 315 | 316 | void navigationTapped(int page) { 317 | //Animating Page 318 | pageController.jumpToPage(page); 319 | } 320 | 321 | void onPageChanged(int page) { 322 | setState(() { 323 | this._page = page; 324 | }); 325 | } 326 | 327 | @override 328 | void initState() { 329 | super.initState(); 330 | Firebase.initializeApp().then((_) { 331 | setState(() { 332 | firebaseInitialized= true; 333 | }); 334 | }); 335 | pageController = PageController(); 336 | } 337 | 338 | @override 339 | void dispose() { 340 | super.dispose(); 341 | pageController.dispose(); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | class User { 4 | final String email; 5 | final String id; 6 | final String photoUrl; 7 | final String username; 8 | final String displayName; 9 | final String bio; 10 | final Map followers; 11 | final Map following; 12 | 13 | const User( 14 | {this.username, 15 | this.id, 16 | this.photoUrl, 17 | this.email, 18 | this.displayName, 19 | this.bio, 20 | this.followers, 21 | this.following}); 22 | 23 | factory User.fromDocument(DocumentSnapshot document) { 24 | return User( 25 | email: document['email'], 26 | username: document['username'], 27 | photoUrl: document['photoUrl'], 28 | id: document.id, 29 | displayName: document['displayName'], 30 | bio: document['bio'], 31 | followers: document['followers'], 32 | following: document['following'], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'main.dart'; 4 | import 'image_post.dart'; 5 | import 'dart:async'; 6 | import 'edit_profile_page.dart'; 7 | import 'models/user.dart'; 8 | 9 | class ProfilePage extends StatefulWidget { 10 | const ProfilePage({this.userId}); 11 | 12 | final String userId; 13 | 14 | _ProfilePage createState() => _ProfilePage(this.userId); 15 | } 16 | 17 | class _ProfilePage extends State 18 | with AutomaticKeepAliveClientMixin { 19 | final String profileId; 20 | String currentUserId = googleSignIn.currentUser.id; 21 | String view = "grid"; // default view 22 | bool isFollowing = false; 23 | bool followButtonClicked = false; 24 | int postCount = 0; 25 | int followerCount = 0; 26 | int followingCount = 0; 27 | _ProfilePage(this.profileId); 28 | 29 | editProfile() { 30 | EditProfilePage editPage = EditProfilePage(); 31 | 32 | Navigator.of(context) 33 | .push(MaterialPageRoute(builder: (BuildContext context) { 34 | return Center( 35 | child: Scaffold( 36 | appBar: AppBar( 37 | leading: IconButton( 38 | icon: Icon(Icons.close), 39 | onPressed: () { 40 | Navigator.maybePop(context); 41 | }, 42 | ), 43 | title: Text('Edit Profile', 44 | style: TextStyle( 45 | color: Colors.black, fontWeight: FontWeight.bold)), 46 | backgroundColor: Colors.white, 47 | actions: [ 48 | IconButton( 49 | icon: Icon( 50 | Icons.check, 51 | color: Colors.blueAccent, 52 | ), 53 | onPressed: () { 54 | editPage.applyChanges(); 55 | Navigator.maybePop(context); 56 | }) 57 | ], 58 | ), 59 | body: ListView( 60 | children: [ 61 | Container( 62 | child: editPage, 63 | ), 64 | ], 65 | )), 66 | ); 67 | })); 68 | } 69 | 70 | followUser() { 71 | print('following user'); 72 | setState(() { 73 | this.isFollowing = true; 74 | followButtonClicked = true; 75 | }); 76 | 77 | FirebaseFirestore.instance.doc("insta_users/$profileId").update({ 78 | 'followers.$currentUserId': true 79 | //firestore plugin doesnt support deleting, so it must be nulled / falsed 80 | }); 81 | 82 | FirebaseFirestore.instance.doc("insta_users/$currentUserId").update({ 83 | 'following.$profileId': true 84 | //firestore plugin doesnt support deleting, so it must be nulled / falsed 85 | }); 86 | 87 | //updates activity feed 88 | FirebaseFirestore.instance 89 | .collection("insta_a_feed") 90 | .doc(profileId) 91 | .collection("items") 92 | .doc(currentUserId) 93 | .set({ 94 | "ownerId": profileId, 95 | "username": currentUserModel.username, 96 | "userId": currentUserId, 97 | "type": "follow", 98 | "userProfileImg": currentUserModel.photoUrl, 99 | "timestamp": DateTime.now() 100 | }); 101 | } 102 | 103 | unfollowUser() { 104 | setState(() { 105 | isFollowing = false; 106 | followButtonClicked = true; 107 | }); 108 | 109 | FirebaseFirestore.instance.doc("insta_users/$profileId").update({ 110 | 'followers.$currentUserId': false 111 | //firestore plugin doesnt support deleting, so it must be nulled / falsed 112 | }); 113 | 114 | FirebaseFirestore.instance.doc("insta_users/$currentUserId").update({ 115 | 'following.$profileId': false 116 | //firestore plugin doesnt support deleting, so it must be nulled / falsed 117 | }); 118 | 119 | FirebaseFirestore.instance 120 | .collection("insta_a_feed") 121 | .doc(profileId) 122 | .collection("items") 123 | .doc(currentUserId) 124 | .delete(); 125 | } 126 | 127 | @override 128 | Widget build(BuildContext context) { 129 | super.build(context); // reloads state when opened again 130 | 131 | Column buildStatColumn(String label, int number) { 132 | return Column( 133 | mainAxisSize: MainAxisSize.min, 134 | mainAxisAlignment: MainAxisAlignment.center, 135 | children: [ 136 | Text( 137 | number.toString(), 138 | style: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold), 139 | ), 140 | Container( 141 | margin: const EdgeInsets.only(top: 4.0), 142 | child: Text( 143 | label, 144 | style: TextStyle( 145 | color: Colors.grey, 146 | fontSize: 15.0, 147 | fontWeight: FontWeight.w400), 148 | )) 149 | ], 150 | ); 151 | } 152 | 153 | Container buildFollowButton( 154 | {String text, 155 | Color backgroundcolor, 156 | Color textColor, 157 | Color borderColor, 158 | Function function}) { 159 | return Container( 160 | padding: EdgeInsets.only(top: 2.0), 161 | child: FlatButton( 162 | onPressed: function, 163 | child: Container( 164 | decoration: BoxDecoration( 165 | color: backgroundcolor, 166 | border: Border.all(color: borderColor), 167 | borderRadius: BorderRadius.circular(5.0)), 168 | alignment: Alignment.center, 169 | child: Text(text, 170 | style: TextStyle( 171 | color: textColor, fontWeight: FontWeight.bold)), 172 | width: 250.0, 173 | height: 27.0, 174 | )), 175 | ); 176 | } 177 | 178 | Container buildProfileFollowButton(User user) { 179 | // viewing your own profile - should show edit button 180 | if (currentUserId == profileId) { 181 | return buildFollowButton( 182 | text: "Edit Profile", 183 | backgroundcolor: Colors.white, 184 | textColor: Colors.black, 185 | borderColor: Colors.grey, 186 | function: editProfile, 187 | ); 188 | } 189 | 190 | // already following user - should show unfollow button 191 | if (isFollowing) { 192 | return buildFollowButton( 193 | text: "Unfollow", 194 | backgroundcolor: Colors.white, 195 | textColor: Colors.black, 196 | borderColor: Colors.grey, 197 | function: unfollowUser, 198 | ); 199 | } 200 | 201 | // does not follow user - should show follow button 202 | if (!isFollowing) { 203 | return buildFollowButton( 204 | text: "Follow", 205 | backgroundcolor: Colors.blue, 206 | textColor: Colors.white, 207 | borderColor: Colors.blue, 208 | function: followUser, 209 | ); 210 | } 211 | 212 | return buildFollowButton( 213 | text: "loading...", 214 | backgroundcolor: Colors.white, 215 | textColor: Colors.black, 216 | borderColor: Colors.grey); 217 | } 218 | 219 | Row buildImageViewButtonBar() { 220 | Color isActiveButtonColor(String viewName) { 221 | if (view == viewName) { 222 | return Colors.blueAccent; 223 | } else { 224 | return Colors.black26; 225 | } 226 | } 227 | 228 | return Row( 229 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 230 | children: [ 231 | IconButton( 232 | icon: Icon(Icons.grid_on, color: isActiveButtonColor("grid")), 233 | onPressed: () { 234 | changeView("grid"); 235 | }, 236 | ), 237 | IconButton( 238 | icon: Icon(Icons.list, color: isActiveButtonColor("feed")), 239 | onPressed: () { 240 | changeView("feed"); 241 | }, 242 | ), 243 | ], 244 | ); 245 | } 246 | 247 | Container buildUserPosts() { 248 | Future> getPosts() async { 249 | List posts = []; 250 | var snap = await FirebaseFirestore.instance 251 | .collection('insta_posts') 252 | .where('ownerId', isEqualTo: profileId) 253 | .orderBy("timestamp") 254 | .get(); 255 | for (var doc in snap.docs) { 256 | posts.add(ImagePost.fromDocument(doc)); 257 | } 258 | setState(() { 259 | postCount = snap.docs.length; 260 | }); 261 | 262 | return posts.reversed.toList(); 263 | } 264 | 265 | return Container( 266 | child: FutureBuilder>( 267 | future: getPosts(), 268 | builder: (context, snapshot) { 269 | if (!snapshot.hasData) 270 | return Container( 271 | alignment: FractionalOffset.center, 272 | padding: const EdgeInsets.only(top: 10.0), 273 | child: CircularProgressIndicator()); 274 | else if (view == "grid") { 275 | // build the grid 276 | return GridView.count( 277 | crossAxisCount: 3, 278 | childAspectRatio: 1.0, 279 | // padding: const EdgeInsets.all(0.5), 280 | mainAxisSpacing: 1.5, 281 | crossAxisSpacing: 1.5, 282 | shrinkWrap: true, 283 | physics: const NeverScrollableScrollPhysics(), 284 | children: snapshot.data.map((ImagePost imagePost) { 285 | return GridTile(child: ImageTile(imagePost)); 286 | }).toList()); 287 | } else if (view == "feed") { 288 | return Column( 289 | children: snapshot.data.map((ImagePost imagePost) { 290 | return imagePost; 291 | }).toList()); 292 | } 293 | }, 294 | )); 295 | } 296 | 297 | return StreamBuilder( 298 | stream: FirebaseFirestore.instance 299 | .collection('insta_users') 300 | .doc(profileId) 301 | .snapshots(), 302 | builder: (context, snapshot) { 303 | if (!snapshot.hasData) 304 | return Container( 305 | alignment: FractionalOffset.center, 306 | child: CircularProgressIndicator()); 307 | 308 | User user = User.fromDocument(snapshot.data); 309 | 310 | if (user.followers.containsKey(currentUserId) && 311 | user.followers[currentUserId] && 312 | followButtonClicked == false) { 313 | isFollowing = true; 314 | } 315 | 316 | return Scaffold( 317 | appBar: AppBar( 318 | title: Text( 319 | user.username, 320 | style: const TextStyle(color: Colors.black), 321 | ), 322 | backgroundColor: Colors.white, 323 | ), 324 | body: ListView( 325 | children: [ 326 | Padding( 327 | padding: const EdgeInsets.all(16.0), 328 | child: Column( 329 | children: [ 330 | Row( 331 | children: [ 332 | CircleAvatar( 333 | radius: 40.0, 334 | backgroundColor: Colors.grey, 335 | backgroundImage: NetworkImage(user.photoUrl), 336 | ), 337 | Expanded( 338 | flex: 1, 339 | child: Column( 340 | children: [ 341 | Row( 342 | mainAxisSize: MainAxisSize.max, 343 | mainAxisAlignment: 344 | MainAxisAlignment.spaceEvenly, 345 | children: [ 346 | buildStatColumn("posts", postCount), 347 | buildStatColumn("followers", 348 | _countFollowings(user.followers)), 349 | buildStatColumn("following", 350 | _countFollowings(user.following)), 351 | ], 352 | ), 353 | Row( 354 | mainAxisAlignment: 355 | MainAxisAlignment.spaceEvenly, 356 | children: [ 357 | buildProfileFollowButton(user) 358 | ]), 359 | ], 360 | ), 361 | ) 362 | ], 363 | ), 364 | Container( 365 | alignment: Alignment.centerLeft, 366 | padding: const EdgeInsets.only(top: 15.0), 367 | child: Text( 368 | user.displayName, 369 | style: TextStyle(fontWeight: FontWeight.bold), 370 | )), 371 | Container( 372 | alignment: Alignment.centerLeft, 373 | padding: const EdgeInsets.only(top: 1.0), 374 | child: Text(user.bio), 375 | ), 376 | ], 377 | ), 378 | ), 379 | Divider(), 380 | buildImageViewButtonBar(), 381 | Divider(height: 0.0), 382 | buildUserPosts(), 383 | ], 384 | )); 385 | }); 386 | } 387 | 388 | changeView(String viewName) { 389 | setState(() { 390 | view = viewName; 391 | }); 392 | } 393 | 394 | int _countFollowings(Map followings) { 395 | int count = 0; 396 | 397 | void countValues(key, value) { 398 | if (value) { 399 | count += 1; 400 | } 401 | } 402 | 403 | followings.forEach(countValues); 404 | 405 | return count; 406 | } 407 | 408 | // ensures state is kept when switching pages 409 | @override 410 | bool get wantKeepAlive => true; 411 | } 412 | 413 | class ImageTile extends StatelessWidget { 414 | final ImagePost imagePost; 415 | 416 | ImageTile(this.imagePost); 417 | 418 | clickedImage(BuildContext context) { 419 | Navigator.of(context) 420 | .push(MaterialPageRoute(builder: (BuildContext context) { 421 | return Center( 422 | child: Scaffold( 423 | appBar: AppBar( 424 | title: Text('Photo', 425 | style: TextStyle( 426 | color: Colors.black, fontWeight: FontWeight.bold)), 427 | backgroundColor: Colors.white, 428 | ), 429 | body: ListView( 430 | children: [ 431 | Container( 432 | child: imagePost, 433 | ), 434 | ], 435 | )), 436 | ); 437 | })); 438 | } 439 | 440 | Widget build(BuildContext context) { 441 | return GestureDetector( 442 | onTap: () => clickedImage(context), 443 | child: Image.network(imagePost.mediaUrl, fit: BoxFit.cover)); 444 | } 445 | } 446 | 447 | void openProfile(BuildContext context, String userId) { 448 | Navigator.of(context) 449 | .push(MaterialPageRoute(builder: (BuildContext context) { 450 | return ProfilePage(userId: userId); 451 | })); 452 | } 453 | -------------------------------------------------------------------------------- /lib/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'dart:async'; 4 | import "profile_page.dart"; // needed to import for openProfile function 5 | import 'models/user.dart'; 6 | 7 | class SearchPage extends StatefulWidget { 8 | _SearchPage createState() => _SearchPage(); 9 | } 10 | 11 | class _SearchPage extends State with AutomaticKeepAliveClientMixin{ 12 | Future userDocs; 13 | 14 | buildSearchField() { 15 | return AppBar( 16 | backgroundColor: Colors.white, 17 | title: Form( 18 | child: TextFormField( 19 | decoration: InputDecoration(labelText: 'Search for a user...'), 20 | onFieldSubmitted: submit, 21 | ), 22 | ), 23 | ); 24 | } 25 | 26 | ListView buildSearchResults(List docs) { 27 | List userSearchItems = []; 28 | 29 | docs.forEach((DocumentSnapshot doc) { 30 | User user = User.fromDocument(doc); 31 | UserSearchItem searchItem = UserSearchItem(user); 32 | userSearchItems.add(searchItem); 33 | }); 34 | 35 | return ListView( 36 | children: userSearchItems, 37 | ); 38 | } 39 | 40 | void submit(String searchValue) async { 41 | Future users = FirebaseFirestore.instance 42 | .collection("insta_users") 43 | .where('displayName', isGreaterThanOrEqualTo: searchValue) 44 | .get(); 45 | 46 | setState(() { 47 | userDocs = users; 48 | }); 49 | } 50 | 51 | Widget build(BuildContext context) { 52 | super.build(context); // reloads state when opened again 53 | 54 | return Scaffold( 55 | appBar: buildSearchField(), 56 | body: userDocs == null 57 | ? Text("") 58 | : FutureBuilder( 59 | future: userDocs, 60 | builder: (context, snapshot) { 61 | if (snapshot.hasData) { 62 | return buildSearchResults(snapshot.data.docs); 63 | } else { 64 | return Container( 65 | alignment: FractionalOffset.center, 66 | child: CircularProgressIndicator()); 67 | } 68 | }), 69 | ); 70 | } 71 | 72 | // ensures state is kept when switching pages 73 | @override 74 | bool get wantKeepAlive => true; 75 | } 76 | 77 | class UserSearchItem extends StatelessWidget { 78 | final User user; 79 | 80 | const UserSearchItem(this.user); 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | TextStyle boldStyle = TextStyle( 85 | color: Colors.black, 86 | fontWeight: FontWeight.bold, 87 | ); 88 | 89 | return GestureDetector( 90 | child: ListTile( 91 | leading: CircleAvatar( 92 | backgroundImage: NetworkImage(user.photoUrl), 93 | backgroundColor: Colors.grey, 94 | ), 95 | title: Text(user.username, style: boldStyle), 96 | subtitle: Text(user.displayName), 97 | ), 98 | onTap: () { 99 | openProfile(context, user.id); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/upload_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | import 'package:firebase_storage/firebase_storage.dart'; 4 | import 'package:cloud_firestore/cloud_firestore.dart'; 5 | import 'package:uuid/uuid.dart'; 6 | import 'dart:async'; 7 | import 'main.dart'; 8 | import 'dart:io'; 9 | import 'location.dart'; 10 | import 'package:geocoder/geocoder.dart'; 11 | 12 | class Uploader extends StatefulWidget { 13 | _Uploader createState() => _Uploader(); 14 | } 15 | 16 | class _Uploader extends State { 17 | File file; 18 | //Strings required to save address 19 | Address address; 20 | 21 | Map currentLocation = Map(); 22 | TextEditingController descriptionController = TextEditingController(); 23 | TextEditingController locationController = TextEditingController(); 24 | ImagePicker imagePicker = ImagePicker(); 25 | 26 | bool uploading = false; 27 | 28 | @override 29 | initState() { 30 | //variables with location assigned as 0.0 31 | currentLocation['latitude'] = 0.0; 32 | currentLocation['longitude'] = 0.0; 33 | initPlatformState(); //method to call location 34 | super.initState(); 35 | } 36 | 37 | //method to get Location and save into variables 38 | initPlatformState() async { 39 | Address first = await getUserLocation(); 40 | setState(() { 41 | address = first; 42 | }); 43 | } 44 | 45 | Widget build(BuildContext context) { 46 | return file == null 47 | ? IconButton( 48 | icon: Icon(Icons.file_upload), 49 | onPressed: () => {_selectImage(context)}) 50 | : Scaffold( 51 | resizeToAvoidBottomInset: false, 52 | appBar: AppBar( 53 | backgroundColor: Colors.white70, 54 | leading: IconButton( 55 | icon: Icon(Icons.arrow_back, color: Colors.black), 56 | onPressed: clearImage), 57 | title: const Text( 58 | 'Post to', 59 | style: const TextStyle(color: Colors.black), 60 | ), 61 | actions: [ 62 | FlatButton( 63 | onPressed: postImage, 64 | child: Text( 65 | "Post", 66 | style: TextStyle( 67 | color: Colors.blueAccent, 68 | fontWeight: FontWeight.bold, 69 | fontSize: 20.0), 70 | )) 71 | ], 72 | ), 73 | body: ListView( 74 | children: [ 75 | PostForm( 76 | imageFile: file, 77 | descriptionController: descriptionController, 78 | locationController: locationController, 79 | loading: uploading, 80 | ), 81 | Divider(), //scroll view where we will show location to users 82 | (address == null) 83 | ? Container() 84 | : SingleChildScrollView( 85 | scrollDirection: Axis.horizontal, 86 | padding: EdgeInsets.only(right: 5.0, left: 5.0), 87 | child: Row( 88 | children: [ 89 | buildLocationButton(address.featureName), 90 | buildLocationButton(address.subLocality), 91 | buildLocationButton(address.locality), 92 | buildLocationButton(address.subAdminArea), 93 | buildLocationButton(address.adminArea), 94 | buildLocationButton(address.countryName), 95 | ], 96 | ), 97 | ), 98 | (address == null) ? Container() : Divider(), 99 | ], 100 | )); 101 | } 102 | 103 | //method to build buttons with location. 104 | buildLocationButton(String locationName) { 105 | if (locationName != null ?? locationName.isNotEmpty) { 106 | return InkWell( 107 | onTap: () { 108 | locationController.text = locationName; 109 | }, 110 | child: Center( 111 | child: Container( 112 | //width: 100.0, 113 | height: 30.0, 114 | padding: EdgeInsets.only(left: 8.0, right: 8.0), 115 | margin: EdgeInsets.only(right: 3.0, left: 3.0), 116 | decoration: BoxDecoration( 117 | color: Colors.grey[200], 118 | borderRadius: BorderRadius.circular(5.0), 119 | ), 120 | child: Center( 121 | child: Text( 122 | locationName, 123 | style: TextStyle(color: Colors.grey), 124 | ), 125 | ), 126 | ), 127 | ), 128 | ); 129 | } else { 130 | return Container(); 131 | } 132 | } 133 | 134 | _selectImage(BuildContext parentContext) async { 135 | return showDialog( 136 | context: parentContext, 137 | barrierDismissible: false, // user must tap button! 138 | 139 | builder: (BuildContext context) { 140 | return SimpleDialog( 141 | title: const Text('Create a Post'), 142 | children: [ 143 | SimpleDialogOption( 144 | child: const Text('Take a photo'), 145 | onPressed: () async { 146 | Navigator.pop(context); 147 | PickedFile imageFile = 148 | await imagePicker.getImage(source: ImageSource.camera, maxWidth: 1920, maxHeight: 1200, imageQuality: 80); 149 | setState(() { 150 | file = File(imageFile.path); 151 | }); 152 | }), 153 | SimpleDialogOption( 154 | child: const Text('Choose from Gallery'), 155 | onPressed: () async { 156 | Navigator.of(context).pop(); 157 | PickedFile imageFile = 158 | await imagePicker.getImage(source: ImageSource.gallery, maxWidth: 1920, maxHeight: 1200, imageQuality: 80); 159 | setState(() { 160 | file = File(imageFile.path); 161 | }); 162 | }), 163 | SimpleDialogOption( 164 | child: const Text("Cancel"), 165 | onPressed: () { 166 | Navigator.pop(context); 167 | }, 168 | ) 169 | ], 170 | ); 171 | }, 172 | ); 173 | } 174 | 175 | void clearImage() { 176 | setState(() { 177 | file = null; 178 | }); 179 | } 180 | 181 | void postImage() { 182 | setState(() { 183 | uploading = true; 184 | }); 185 | uploadImage(file).then((String data) { 186 | postToFireStore( 187 | mediaUrl: data, 188 | description: descriptionController.text, 189 | location: locationController.text); 190 | }).then((_) { 191 | setState(() { 192 | file = null; 193 | uploading = false; 194 | }); 195 | }); 196 | } 197 | } 198 | 199 | class PostForm extends StatelessWidget { 200 | final imageFile; 201 | final TextEditingController descriptionController; 202 | final TextEditingController locationController; 203 | final bool loading; 204 | PostForm( 205 | {this.imageFile, 206 | this.descriptionController, 207 | this.loading, 208 | this.locationController}); 209 | 210 | Widget build(BuildContext context) { 211 | return Column( 212 | children: [ 213 | loading 214 | ? LinearProgressIndicator() 215 | : Padding(padding: EdgeInsets.only(top: 0.0)), 216 | Divider(), 217 | Row( 218 | mainAxisAlignment: MainAxisAlignment.spaceAround, 219 | children: [ 220 | CircleAvatar( 221 | backgroundImage: NetworkImage(currentUserModel.photoUrl), 222 | ), 223 | Container( 224 | width: 250.0, 225 | child: TextField( 226 | controller: descriptionController, 227 | decoration: InputDecoration( 228 | hintText: "Write a caption...", border: InputBorder.none), 229 | ), 230 | ), 231 | Container( 232 | height: 45.0, 233 | width: 45.0, 234 | child: AspectRatio( 235 | aspectRatio: 487 / 451, 236 | child: Container( 237 | decoration: BoxDecoration( 238 | image: DecorationImage( 239 | fit: BoxFit.fill, 240 | alignment: FractionalOffset.topCenter, 241 | image: FileImage(imageFile), 242 | )), 243 | ), 244 | ), 245 | ), 246 | ], 247 | ), 248 | Divider(), 249 | ListTile( 250 | leading: Icon(Icons.pin_drop), 251 | title: Container( 252 | width: 250.0, 253 | child: TextField( 254 | controller: locationController, 255 | decoration: InputDecoration( 256 | hintText: "Where was this photo taken?", 257 | border: InputBorder.none), 258 | ), 259 | ), 260 | ) 261 | ], 262 | ); 263 | } 264 | } 265 | 266 | Future uploadImage(var imageFile) async { 267 | var uuid = Uuid().v1(); 268 | Reference ref = FirebaseStorage.instance.ref().child("post_$uuid.jpg"); 269 | UploadTask uploadTask = ref.putFile(imageFile); 270 | 271 | String downloadUrl = await (await uploadTask).ref.getDownloadURL(); 272 | return downloadUrl; 273 | } 274 | 275 | void postToFireStore( 276 | {String mediaUrl, String location, String description}) async { 277 | var reference = FirebaseFirestore.instance.collection('insta_posts'); 278 | 279 | reference.add({ 280 | "username": currentUserModel.username, 281 | "location": location, 282 | "likes": {}, 283 | "mediaUrl": mediaUrl, 284 | "description": description, 285 | "ownerId": googleSignIn.currentUser.id, 286 | "timestamp": DateTime.now(), 287 | }).then((DocumentReference doc) { 288 | String docId = doc.id; 289 | reference.doc(docId).update({"postId": docId}); 290 | }); 291 | } 292 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "3.1.2" 11 | async: 12 | dependency: "direct main" 13 | description: 14 | name: async 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.5.0" 18 | cached_network_image: 19 | dependency: "direct main" 20 | description: 21 | name: cached_network_image 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.5.1" 25 | characters: 26 | dependency: transitive 27 | description: 28 | name: characters 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.0" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.2.0" 39 | clock: 40 | dependency: transitive 41 | description: 42 | name: clock 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.0" 46 | cloud_firestore: 47 | dependency: "direct main" 48 | description: 49 | name: cloud_firestore 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.0.1" 53 | cloud_firestore_platform_interface: 54 | dependency: transitive 55 | description: 56 | name: cloud_firestore_platform_interface 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "4.0.0" 60 | cloud_firestore_web: 61 | dependency: transitive 62 | description: 63 | name: cloud_firestore_web 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.0.1" 67 | collection: 68 | dependency: transitive 69 | description: 70 | name: collection 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.15.0" 74 | crypto: 75 | dependency: transitive 76 | description: 77 | name: crypto 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "3.0.0" 81 | cupertino_icons: 82 | dependency: "direct main" 83 | description: 84 | name: cupertino_icons 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.0.2" 88 | ffi: 89 | dependency: transitive 90 | description: 91 | name: ffi 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "1.0.0" 95 | file: 96 | dependency: transitive 97 | description: 98 | name: file 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "6.1.0" 102 | firebase_auth: 103 | dependency: "direct main" 104 | description: 105 | name: firebase_auth 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.0.1" 109 | firebase_auth_platform_interface: 110 | dependency: transitive 111 | description: 112 | name: firebase_auth_platform_interface 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "4.0.0" 116 | firebase_auth_web: 117 | dependency: transitive 118 | description: 119 | name: firebase_auth_web 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.0.2" 123 | firebase_core: 124 | dependency: "direct main" 125 | description: 126 | name: firebase_core 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "1.0.1" 130 | firebase_core_platform_interface: 131 | dependency: transitive 132 | description: 133 | name: firebase_core_platform_interface 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "4.0.0" 137 | firebase_core_web: 138 | dependency: transitive 139 | description: 140 | name: firebase_core_web 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "1.0.1" 144 | firebase_messaging: 145 | dependency: "direct main" 146 | description: 147 | name: firebase_messaging 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "9.0.0" 151 | firebase_messaging_platform_interface: 152 | dependency: transitive 153 | description: 154 | name: firebase_messaging_platform_interface 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "2.0.0" 158 | firebase_messaging_web: 159 | dependency: transitive 160 | description: 161 | name: firebase_messaging_web 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "1.0.1" 165 | firebase_storage: 166 | dependency: "direct main" 167 | description: 168 | name: firebase_storage 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "8.0.0" 172 | firebase_storage_platform_interface: 173 | dependency: transitive 174 | description: 175 | name: firebase_storage_platform_interface 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "2.0.0" 179 | firebase_storage_web: 180 | dependency: transitive 181 | description: 182 | name: firebase_storage_web 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "1.0.1" 186 | flare_dart: 187 | dependency: transitive 188 | description: 189 | name: flare_dart 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "2.3.4" 193 | flare_flutter: 194 | dependency: "direct main" 195 | description: 196 | name: flare_flutter 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "2.0.6" 200 | flutter: 201 | dependency: "direct main" 202 | description: flutter 203 | source: sdk 204 | version: "0.0.0" 205 | flutter_blurhash: 206 | dependency: transitive 207 | description: 208 | name: flutter_blurhash 209 | url: "https://pub.dartlang.org" 210 | source: hosted 211 | version: "0.5.0" 212 | flutter_cache_manager: 213 | dependency: transitive 214 | description: 215 | name: flutter_cache_manager 216 | url: "https://pub.dartlang.org" 217 | source: hosted 218 | version: "2.1.2" 219 | flutter_plugin_android_lifecycle: 220 | dependency: transitive 221 | description: 222 | name: flutter_plugin_android_lifecycle 223 | url: "https://pub.dartlang.org" 224 | source: hosted 225 | version: "2.0.0" 226 | flutter_web_plugins: 227 | dependency: transitive 228 | description: flutter 229 | source: sdk 230 | version: "0.0.0" 231 | font_awesome_flutter: 232 | dependency: "direct main" 233 | description: 234 | name: font_awesome_flutter 235 | url: "https://pub.dartlang.org" 236 | source: hosted 237 | version: "8.12.0" 238 | geocoder: 239 | dependency: "direct main" 240 | description: 241 | name: geocoder 242 | url: "https://pub.dartlang.org" 243 | source: hosted 244 | version: "0.2.1" 245 | google_sign_in: 246 | dependency: "direct main" 247 | description: 248 | name: google_sign_in 249 | url: "https://pub.dartlang.org" 250 | source: hosted 251 | version: "5.0.0" 252 | google_sign_in_platform_interface: 253 | dependency: transitive 254 | description: 255 | name: google_sign_in_platform_interface 256 | url: "https://pub.dartlang.org" 257 | source: hosted 258 | version: "2.0.1" 259 | google_sign_in_web: 260 | dependency: transitive 261 | description: 262 | name: google_sign_in_web 263 | url: "https://pub.dartlang.org" 264 | source: hosted 265 | version: "0.10.0" 266 | http: 267 | dependency: "direct main" 268 | description: 269 | name: http 270 | url: "https://pub.dartlang.org" 271 | source: hosted 272 | version: "0.13.0" 273 | http_parser: 274 | dependency: transitive 275 | description: 276 | name: http_parser 277 | url: "https://pub.dartlang.org" 278 | source: hosted 279 | version: "4.0.0" 280 | image: 281 | dependency: "direct main" 282 | description: 283 | name: image 284 | url: "https://pub.dartlang.org" 285 | source: hosted 286 | version: "3.0.1" 287 | image_picker: 288 | dependency: "direct main" 289 | description: 290 | name: image_picker 291 | url: "https://pub.dartlang.org" 292 | source: hosted 293 | version: "0.7.2+1" 294 | image_picker_platform_interface: 295 | dependency: transitive 296 | description: 297 | name: image_picker_platform_interface 298 | url: "https://pub.dartlang.org" 299 | source: hosted 300 | version: "2.0.1" 301 | intl: 302 | dependency: transitive 303 | description: 304 | name: intl 305 | url: "https://pub.dartlang.org" 306 | source: hosted 307 | version: "0.17.0" 308 | js: 309 | dependency: transitive 310 | description: 311 | name: js 312 | url: "https://pub.dartlang.org" 313 | source: hosted 314 | version: "0.6.3" 315 | location: 316 | dependency: "direct main" 317 | description: 318 | name: location 319 | url: "https://pub.dartlang.org" 320 | source: hosted 321 | version: "4.1.1" 322 | location_platform_interface: 323 | dependency: transitive 324 | description: 325 | name: location_platform_interface 326 | url: "https://pub.dartlang.org" 327 | source: hosted 328 | version: "2.1.0" 329 | location_web: 330 | dependency: transitive 331 | description: 332 | name: location_web 333 | url: "https://pub.dartlang.org" 334 | source: hosted 335 | version: "3.0.0" 336 | matcher: 337 | dependency: transitive 338 | description: 339 | name: matcher 340 | url: "https://pub.dartlang.org" 341 | source: hosted 342 | version: "0.12.10" 343 | meta: 344 | dependency: transitive 345 | description: 346 | name: meta 347 | url: "https://pub.dartlang.org" 348 | source: hosted 349 | version: "1.3.0" 350 | octo_image: 351 | dependency: transitive 352 | description: 353 | name: octo_image 354 | url: "https://pub.dartlang.org" 355 | source: hosted 356 | version: "0.3.0" 357 | path: 358 | dependency: transitive 359 | description: 360 | name: path 361 | url: "https://pub.dartlang.org" 362 | source: hosted 363 | version: "1.8.0" 364 | path_provider: 365 | dependency: transitive 366 | description: 367 | name: path_provider 368 | url: "https://pub.dartlang.org" 369 | source: hosted 370 | version: "2.0.1" 371 | path_provider_linux: 372 | dependency: transitive 373 | description: 374 | name: path_provider_linux 375 | url: "https://pub.dartlang.org" 376 | source: hosted 377 | version: "2.0.0" 378 | path_provider_macos: 379 | dependency: transitive 380 | description: 381 | name: path_provider_macos 382 | url: "https://pub.dartlang.org" 383 | source: hosted 384 | version: "2.0.0" 385 | path_provider_platform_interface: 386 | dependency: transitive 387 | description: 388 | name: path_provider_platform_interface 389 | url: "https://pub.dartlang.org" 390 | source: hosted 391 | version: "2.0.1" 392 | path_provider_windows: 393 | dependency: transitive 394 | description: 395 | name: path_provider_windows 396 | url: "https://pub.dartlang.org" 397 | source: hosted 398 | version: "2.0.0" 399 | pedantic: 400 | dependency: transitive 401 | description: 402 | name: pedantic 403 | url: "https://pub.dartlang.org" 404 | source: hosted 405 | version: "1.11.0" 406 | petitparser: 407 | dependency: transitive 408 | description: 409 | name: petitparser 410 | url: "https://pub.dartlang.org" 411 | source: hosted 412 | version: "4.0.2" 413 | platform: 414 | dependency: transitive 415 | description: 416 | name: platform 417 | url: "https://pub.dartlang.org" 418 | source: hosted 419 | version: "3.0.0" 420 | plugin_platform_interface: 421 | dependency: transitive 422 | description: 423 | name: plugin_platform_interface 424 | url: "https://pub.dartlang.org" 425 | source: hosted 426 | version: "2.0.0" 427 | process: 428 | dependency: transitive 429 | description: 430 | name: process 431 | url: "https://pub.dartlang.org" 432 | source: hosted 433 | version: "4.1.0" 434 | quiver: 435 | dependency: transitive 436 | description: 437 | name: quiver 438 | url: "https://pub.dartlang.org" 439 | source: hosted 440 | version: "3.0.0" 441 | rxdart: 442 | dependency: transitive 443 | description: 444 | name: rxdart 445 | url: "https://pub.dartlang.org" 446 | source: hosted 447 | version: "0.25.0" 448 | shared_preferences: 449 | dependency: "direct main" 450 | description: 451 | name: shared_preferences 452 | url: "https://pub.dartlang.org" 453 | source: hosted 454 | version: "2.0.4" 455 | shared_preferences_linux: 456 | dependency: transitive 457 | description: 458 | name: shared_preferences_linux 459 | url: "https://pub.dartlang.org" 460 | source: hosted 461 | version: "2.0.0" 462 | shared_preferences_macos: 463 | dependency: transitive 464 | description: 465 | name: shared_preferences_macos 466 | url: "https://pub.dartlang.org" 467 | source: hosted 468 | version: "2.0.0" 469 | shared_preferences_platform_interface: 470 | dependency: transitive 471 | description: 472 | name: shared_preferences_platform_interface 473 | url: "https://pub.dartlang.org" 474 | source: hosted 475 | version: "2.0.0" 476 | shared_preferences_web: 477 | dependency: transitive 478 | description: 479 | name: shared_preferences_web 480 | url: "https://pub.dartlang.org" 481 | source: hosted 482 | version: "2.0.0" 483 | shared_preferences_windows: 484 | dependency: transitive 485 | description: 486 | name: shared_preferences_windows 487 | url: "https://pub.dartlang.org" 488 | source: hosted 489 | version: "2.0.0" 490 | sky_engine: 491 | dependency: transitive 492 | description: flutter 493 | source: sdk 494 | version: "0.0.99" 495 | source_span: 496 | dependency: transitive 497 | description: 498 | name: source_span 499 | url: "https://pub.dartlang.org" 500 | source: hosted 501 | version: "1.8.1" 502 | sqflite: 503 | dependency: transitive 504 | description: 505 | name: sqflite 506 | url: "https://pub.dartlang.org" 507 | source: hosted 508 | version: "2.0.0+2" 509 | sqflite_common: 510 | dependency: transitive 511 | description: 512 | name: sqflite_common 513 | url: "https://pub.dartlang.org" 514 | source: hosted 515 | version: "2.0.0+2" 516 | stack_trace: 517 | dependency: transitive 518 | description: 519 | name: stack_trace 520 | url: "https://pub.dartlang.org" 521 | source: hosted 522 | version: "1.10.0" 523 | string_scanner: 524 | dependency: transitive 525 | description: 526 | name: string_scanner 527 | url: "https://pub.dartlang.org" 528 | source: hosted 529 | version: "1.1.0" 530 | synchronized: 531 | dependency: transitive 532 | description: 533 | name: synchronized 534 | url: "https://pub.dartlang.org" 535 | source: hosted 536 | version: "3.0.0" 537 | term_glyph: 538 | dependency: transitive 539 | description: 540 | name: term_glyph 541 | url: "https://pub.dartlang.org" 542 | source: hosted 543 | version: "1.2.0" 544 | typed_data: 545 | dependency: transitive 546 | description: 547 | name: typed_data 548 | url: "https://pub.dartlang.org" 549 | source: hosted 550 | version: "1.3.0" 551 | uuid: 552 | dependency: "direct main" 553 | description: 554 | name: uuid 555 | url: "https://pub.dartlang.org" 556 | source: hosted 557 | version: "3.0.1" 558 | vector_math: 559 | dependency: transitive 560 | description: 561 | name: vector_math 562 | url: "https://pub.dartlang.org" 563 | source: hosted 564 | version: "2.1.0" 565 | win32: 566 | dependency: transitive 567 | description: 568 | name: win32 569 | url: "https://pub.dartlang.org" 570 | source: hosted 571 | version: "2.0.4" 572 | xdg_directories: 573 | dependency: transitive 574 | description: 575 | name: xdg_directories 576 | url: "https://pub.dartlang.org" 577 | source: hosted 578 | version: "0.2.0" 579 | xml: 580 | dependency: transitive 581 | description: 582 | name: xml 583 | url: "https://pub.dartlang.org" 584 | source: hosted 585 | version: "5.0.2" 586 | sdks: 587 | dart: ">=2.12.0 <3.0.0" 588 | flutter: ">=1.24.0-10" 589 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: Fluttergram 2 | description: A new Flutter application. 3 | version: 1.2.3+1 4 | 5 | environment: 6 | sdk: '>=2.10.0 <3.0.0' 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | firebase_core: ^1.0.1 12 | cloud_firestore: ^1.0.1 13 | image_picker: ^0.7.2+1 14 | firebase_storage: ^8.0.0 15 | firebase_auth: ^1.0.1 16 | google_sign_in: ^5.0.0 17 | uuid: ^3.0.1 18 | image: ^3.0.1 19 | font_awesome_flutter: ^8.5.0 20 | async: ^2.4.0 21 | http: ^0.13.0 22 | shared_preferences: ^2.0.4 23 | cached_network_image: ^2.0.0 24 | firebase_messaging: ^9.0.0 25 | location: ^4.1.1 26 | geocoder: ^0.2.1 27 | flare_flutter: ^2.0.6 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | 33 | #dev_dependencies: 34 | # flutter_test: 35 | # sdk: flutter 36 | 37 | 38 | # For information on the generic Dart part of this file, see the 39 | # following page: https://www.dartlang.org/tools/pub/pubspec 40 | 41 | # The following section is specific to Flutter. 42 | flutter: 43 | 44 | # The following line ensures that the Material Icons font is 45 | # included with your application, so that you can use the icons in 46 | # the material Icons class. 47 | uses-material-design: true 48 | fonts: 49 | - family: Billabong 50 | fonts: 51 | - asset: assets/fonts/Billabong.ttf 52 | assets: 53 | - assets/images/google_signin_button.png 54 | - assets/flare/Like.flr 55 | # To add assets to your application, add an assets section, like this: 56 | # assets: 57 | # - images/a_dot_burr.jpeg 58 | # - images/a_dot_ham.jpeg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.io/assets-and-images/#resolution-aware. 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.io/assets-and-images/#from-packages 65 | 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.io/custom-fonts/#from-packages 85 | --------------------------------------------------------------------------------