├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── demo.gif ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── flow.png ├── functions ├── .eslintrc.js ├── .gitignore ├── package-lock.json ├── package.json ├── src │ └── index.ts ├── tsconfig.dev.json └── tsconfig.json ├── lib ├── app_state.dart ├── blocs │ ├── app_cubit.dart │ ├── app_events.dart │ └── app_states.dart ├── main.dart ├── models │ └── prediction_model.dart ├── repository │ └── prediction_repo.dart ├── services │ └── backend_service.dart └── ui │ ├── draw │ ├── canvas.dart │ ├── drawing_page.dart │ ├── stroke.dart │ └── stroke_options.dart │ ├── home │ └── home_page.dart │ └── result │ └── result_page.dart ├── pubspec.lock ├── pubspec.yaml ├── sample_01.png ├── sample_02.png ├── sample_03.png ├── storage.rules └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | /resources 47 | /lib/firebase_options.dart 48 | .firebaserc 49 | .firebase/ 50 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 9944297138845a94256f1cf37beb88ff9a8e811a 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 17 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 18 | - platform: android 19 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 20 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 21 | - platform: ios 22 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 23 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 24 | - platform: linux 25 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 26 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 27 | - platform: macos 28 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 29 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 30 | - platform: web 31 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 32 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 33 | - platform: windows 34 | create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 35 | base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Scribble Diffusion 2 | 3 | Turn your scribbles into detailed images with AI. 4 | 5 | Demo: [flutter-scribble.web.app](https://flutter-scribble.web.app) 6 | 7 | --- 8 | Flutter Scribble Diffusion Demo 9 | 10 | ## Sample Generations 11 | 12 | [![A cute clownfish swimming in the ocean](sample_03.png)](https://flutter-scribble.web.app/#/share/mezhk5rvdzbtrg3lriiufkr7sy) 13 | [link](https://flutter-scribble.web.app/#/share/mezhk5rvdzbtrg3lriiufkr7sy) 14 | 15 | [![A Futuristic Spaceship](sample_02.png)](https://flutter-scribble.web.app/#/share/whf7xnpqcnb5bj6udndqde7sbq) 16 | [link](https://flutter-scribble.web.app/#/share/whf7xnpqcnb5bj6udndqde7sbq) 17 | 18 | [![An Ancient Map from the LOTRs](sample_01.png)](https://flutter-scribble.web.app/#/share/4e425d3xdbftpgry6c4ta3asqe) 19 | [link](https://flutter-scribble.web.app/#/share/4e425d3xdbftpgry6c4ta3asqe) 20 | 21 | ## Powered by 22 | 23 | 🚀 [Replicate](https://replicate.com/), a platform for running machine learning models in the cloud. 24 | 25 | 🖍️ [ControlNet](https://replicate.com/jagilley/controlnet-scribble/), an open-source machine learning model that generates images from text and scribbles. 26 | 27 | 🐦 [Flutter](https://flutter.dev/), an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase. 28 | 29 | 🔥 [Firebase](https://firebase.google.com/), an app development platform that helps you build and grow apps and games users love. 30 | 31 | 🔥 ⚡ [Cloud Functions for Firebase](https://firebase.google.com/docs/functions), for talking to the Replicate API. 32 | 33 | 🔥 📝 [Cloud Firestore](https://firebase.google.com/products/firestore), for storing predictions from Replicate. 34 | 35 | 🔥 ☁️ [Cloud Storage for Firebase](https://firebase.google.com/products/storage), for saving generated images from Replicate. 36 | 37 | 🔥 🔐 [Firebase Authentication](https://firebase.google.com/docs/auth), for handling anonymous authentication. 38 | 39 | 🔥 🕸️ [Firebase Hosting](https://firebase.google.com/docs/hosting), for hosting the Flutter web app. 40 | 41 | 🖌️ [Perfect Freehand Dart](https://github.com/steveruizok/perfect-freehand-dart), for scribbles in Flutter. 42 | 43 | --- 44 | 45 | ![Flow](flow.png) 46 | 47 | --- 48 | 49 | Inspired by [🖍️ Scribble Diffusion](https://github.com/replicate/scribble-diffusion) 50 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/demo.gif -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": [ 7 | { 8 | "source": "functions", 9 | "codebase": "default", 10 | "ignore": [ 11 | "node_modules", 12 | ".git", 13 | "firebase-debug.log", 14 | "firebase-debug.*.log" 15 | ], 16 | "predeploy": [ 17 | "npm --prefix \"$RESOURCE_DIR\" run lint", 18 | "npm --prefix \"$RESOURCE_DIR\" run build" 19 | ] 20 | } 21 | ], 22 | "emulators": { 23 | "functions": { 24 | "port": 5001 25 | }, 26 | "firestore": { 27 | "port": 8080 28 | }, 29 | "storage": { 30 | "port": 9199 31 | }, 32 | "ui": { 33 | "enabled": true 34 | }, 35 | "singleProjectMode": true 36 | }, 37 | "storage": { 38 | "rules": "storage.rules" 39 | }, 40 | "hosting": { 41 | "public": "build/web", 42 | "ignore": [ 43 | "firebase.json", 44 | "**/.*", 45 | "**/node_modules/**" 46 | ], 47 | "rewrites": [ 48 | { 49 | "source": "**", 50 | "destination": "/index.html" 51 | } 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /{document=**} { 5 | allow read, write: if false; 6 | } 7 | match /results/{id} { 8 | allow read: if true; 9 | } 10 | match /showcase/{id} { 11 | allow read: if true; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/flow.png -------------------------------------------------------------------------------- /functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | 'google', 13 | 'plugin:@typescript-eslint/recommended', 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | project: ['tsconfig.json', 'tsconfig.dev.json'], 18 | sourceType: 'module', 19 | }, 20 | ignorePatterns: [ 21 | '/lib/**/*', // Ignore built files. 22 | ], 23 | plugins: ['@typescript-eslint', 'import'], 24 | rules: { 25 | quotes: ['error', 'single'], 26 | 'import/no-unresolved': 0, 27 | indent: ['error', 2], 28 | 'object-curly-spacing': ['error', 'always'], 29 | 'quote-props': ['error', 'as-needed'], 30 | 'require-jsdoc': [ 31 | 'error', 32 | { 33 | require: { 34 | FunctionDeclaration: false, 35 | MethodDefinition: false, 36 | ClassDeclaration: false, 37 | ArrowFunctionExpression: false, 38 | FunctionExpression: false, 39 | }, 40 | }, 41 | ], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | lib/**/*.js 3 | lib/**/*.js.map 4 | 5 | # TypeScript v1 declaration files 6 | typings/ 7 | 8 | # Node.js dependency directory 9 | node_modules/ 10 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "eslint --ext .js,.ts .", 5 | "build": "tsc", 6 | "build:watch": "tsc --watch", 7 | "serve": "npm run build && firebase emulators:start --only functions", 8 | "shell": "npm run build && firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "firebase deploy --only functions", 11 | "logs": "firebase functions:log" 12 | }, 13 | "engines": { 14 | "node": "18" 15 | }, 16 | "main": "lib/index.js", 17 | "dependencies": { 18 | "firebase-admin": "^11.5.0", 19 | "firebase-functions": "^4.2.0" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^5.12.0", 23 | "@typescript-eslint/parser": "^5.12.0", 24 | "eslint": "^8.9.0", 25 | "eslint-config-google": "^0.14.0", 26 | "eslint-plugin-import": "^2.25.4", 27 | "firebase-functions-test": "^3.0.0", 28 | "typescript": "^4.9.0" 29 | }, 30 | "private": true 31 | } 32 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import { onRequest } from 'firebase-functions/v2/https'; 2 | import * as functions from 'firebase-functions'; 3 | import { getStorage } from 'firebase-admin/storage'; 4 | import { initializeApp } from 'firebase-admin/app'; 5 | import { getFirestore, FieldValue } from 'firebase-admin/firestore'; 6 | import { randomUUID } from 'crypto'; 7 | 8 | initializeApp(); 9 | 10 | exports.replicatehook = onRequest( 11 | { timeoutSeconds: 20, cors: true }, 12 | async (req, res) => { 13 | if (req.method !== 'POST') { 14 | res.status(403).send('Forbidden!'); 15 | return; 16 | } 17 | 18 | const prediction = req.body; 19 | if ( 20 | prediction?.id === null || 21 | typeof prediction?.id === 'undefined' || 22 | prediction?.id === '' 23 | ) { 24 | res.status(400).send('Bad Request!'); 25 | return; 26 | } 27 | 28 | try { 29 | await getFirestore() 30 | .collection('predictions') 31 | .doc(prediction.id) 32 | .update(prediction); 33 | } catch (err) { 34 | res.status(500).send(err); 35 | return; 36 | } 37 | 38 | const status = prediction.status; 39 | if (status in ['succeeded', 'failed'] === false) { 40 | if (status === 'succeeded') { 41 | const outputImageUrl = prediction?.output[1]; 42 | console.log('outputImageUrl', outputImageUrl); 43 | try { 44 | await processOutputImage(outputImageUrl, prediction.id); 45 | } catch (err) { 46 | console.log(err); 47 | res.status(200).send('Ok'); 48 | return; 49 | } 50 | } 51 | // if (status === 'failed') cleanup 52 | } 53 | 54 | res.status(200).send('Ok'); 55 | } 56 | ); 57 | 58 | async function processOutputImage(url: string, id: string): Promise { 59 | const response = await fetch(url); 60 | const imageData = Buffer.from(await response.arrayBuffer()); 61 | const imageUrl = await uploadImageBuffer(imageData); 62 | 63 | await getFirestore() 64 | .collection('results') 65 | .doc(id) 66 | .update({ output: imageUrl }); 67 | } 68 | 69 | exports.createPrediction = functions 70 | .runWith({ 71 | secrets: ['REPLICATE_API_TOKEN', 'REPLICATE_WEB_HOOK'], 72 | timeoutSeconds: 20, 73 | }) 74 | .https.onCall(async (data: any, context: any) => { 75 | const imageData = data.image; 76 | const prompt = data.prompt; 77 | const userId = data.userId; 78 | 79 | const apiKey = process.env.REPLICATE_API_TOKEN ?? ''; 80 | const webhook = process.env.REPLICATE_WEB_HOOK ?? ''; 81 | 82 | let prediction; 83 | 84 | try { 85 | const imageUrl = await uploadImage(imageData); 86 | prediction = await predict(prompt, imageUrl, apiKey, webhook); 87 | 88 | await getFirestore() 89 | .collection('predictions') 90 | .doc(prediction.id) 91 | .set(prediction); 92 | await getFirestore() 93 | .collection('results') 94 | .doc(prediction.id) 95 | .set({ 96 | input: imageUrl, 97 | output: null, 98 | prompt, 99 | user: userId, 100 | timeStamp: FieldValue.serverTimestamp(), 101 | }); 102 | } catch (error) { 103 | return { error }; 104 | } 105 | 106 | return { data: prediction.id }; 107 | }); 108 | 109 | /** 110 | * Create a replicate prediction 111 | * @param {string} prompt Text prompt 112 | * @param {string} imageUrl Input image url 113 | * @param {string} apiKey Replicate apikey 114 | * @param {string} webhook Webhook for prediction updates 115 | * @return {Promise} Prediction JSON data 116 | */ 117 | async function predict( 118 | prompt: string, 119 | imageUrl: string, 120 | apiKey: string, 121 | webhook: string 122 | ): Promise { 123 | const response = await fetch('https://api.replicate.com/v1/predictions', { 124 | method: 'POST', 125 | headers: { 126 | Authorization: `Token ${apiKey}`, 127 | 'Content-Type': 'application/json', 128 | }, 129 | body: JSON.stringify({ 130 | version: 131 | '435061a1b5a4c1e26740464bf786efdfa9cb3a3ac488595a2de23e143fdb0117', 132 | input: { prompt, image: imageUrl }, 133 | webhook, 134 | webhook_events_filter: ['start', 'completed'], 135 | }), 136 | }); 137 | 138 | if (response.status !== 201) { 139 | const error = await response.json(); 140 | throw Error(`${error.detail}`); 141 | } 142 | 143 | return response.json(); 144 | } 145 | 146 | /** 147 | * Upload 64bitEncoded Image to Storage 148 | * @param {string} imageBytes64Str Image data 149 | * @return {Promise} Image URL 150 | */ 151 | async function uploadImage(imageBytes64Str: string): Promise { 152 | const imageBuffer = Buffer.from(imageBytes64Str, 'base64'); 153 | const imageByteArray = new Uint8Array(imageBuffer); 154 | return await uploadImageBuffer(Buffer.from(imageByteArray)); 155 | } 156 | 157 | async function uploadImageBuffer(buffer: Buffer): Promise { 158 | const bucket = getStorage().bucket(); 159 | const fileName = randomUUID(); 160 | const file = bucket.file(`images/${fileName}.png`); 161 | const options = { resumable: false, metadata: { contentType: 'image/png' } }; 162 | 163 | // options may not be necessary 164 | try { 165 | await file.save(buffer, options); 166 | const urls = await file.getSignedUrl({ 167 | action: 'read', 168 | expires: '03-09-2500', 169 | }); 170 | const url = urls[0]; 171 | return url; 172 | } catch (err) { 173 | throw Error(`Unable to upload image ${err}`); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /functions/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | ".eslintrc.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /lib/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | 5 | class AppState { 6 | AppState() { 7 | StreamController _localStreamController = 8 | StreamController.broadcast(); 9 | } 10 | 11 | void _listenForEntries() { 12 | FirebaseFirestore.instance 13 | .collection('Entries') 14 | .snapshots() 15 | .listen((event) { 16 | final entries = event.docs.map((doc) { 17 | final data = doc.data(); 18 | /*return Entry( 19 | date: data['date'] as String, 20 | text: data['text'] as String, 21 | title: data['title'] as String, 22 | );*/ 23 | }).toList(); 24 | 25 | //_entriesStreamController.add(entries); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/blocs/app_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_scribble/blocs/app_states.dart'; 5 | import 'package:flutter_scribble/models/prediction_model.dart'; 6 | import 'package:flutter_scribble/repository/prediction_repo.dart'; 7 | import 'package:flutter_scribble/services/backend_service.dart'; 8 | import 'package:get_it/get_it.dart'; 9 | 10 | class AppCubit extends Cubit { 11 | final PredictionRepository _predcitionRepository = GetIt.instance(); 12 | final BackendService _backendService = GetIt.instance(); 13 | StreamSubscription? _predictionSubscription; 14 | 15 | AppCubit() : super(InitialState()); 16 | 17 | void init() { 18 | emit(InitialState()); 19 | _backendService.logEvent('screen_view', {'screen_name': 'create_page'}); 20 | } 21 | 22 | Future listenToPrediction(String id) async { 23 | // Subscribe to listen for changes in the prediction state 24 | _predictionSubscription?.cancel(); 25 | _predictionSubscription = _predcitionRepository 26 | .onPredictionUpdated(id) 27 | .listen(_predictionChanged); 28 | } 29 | 30 | Future createPrediction(String imageData, String prompt) async { 31 | final pid = await _backendService.createPrediction(imageData, prompt); 32 | pid == null ? emit(PredictionStopped()) : emit(PredictionStarted(pid)); 33 | _backendService.logEvent('create_prediction', {'prompt': prompt}); 34 | } 35 | 36 | void startPrediction(String prompt) { 37 | emit(PredictionTriggered(prompt)); 38 | } 39 | 40 | void _predictionChanged(PredictionModel data) { 41 | emit(PredictionUpdated(data)); 42 | if (data.outputImageUrl.isNotEmpty) { 43 | emit(PredictionCompleted()); 44 | } 45 | } 46 | 47 | @override 48 | Future close() { 49 | _predictionSubscription?.cancel(); 50 | return super.close(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/blocs/app_events.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class PredictionEvent extends Equatable { 4 | const PredictionEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class GetPredictions extends PredictionEvent { 11 | const GetPredictions(); 12 | } 13 | 14 | class StartPrediction extends PredictionEvent {} 15 | 16 | class EndPrediction extends PredictionEvent {} 17 | 18 | class GetPrediction extends PredictionEvent { 19 | final String id; 20 | const GetPrediction(this.id); 21 | 22 | @override 23 | List get props => [id]; 24 | } 25 | -------------------------------------------------------------------------------- /lib/blocs/app_states.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_scribble/models/prediction_model.dart'; 3 | 4 | abstract class PredictionState extends Equatable { 5 | const PredictionState(); 6 | 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class InitialState extends PredictionState {} 12 | 13 | class PredictionTriggered extends PredictionState { 14 | final String prompt; 15 | const PredictionTriggered(this.prompt); 16 | 17 | @override 18 | List get props => [prompt]; 19 | } 20 | 21 | class PredictionStarted extends PredictionState { 22 | final String id; 23 | const PredictionStarted(this.id); 24 | 25 | @override 26 | List get props => [id]; 27 | } 28 | 29 | class PredictionUpdated extends PredictionState { 30 | final PredictionModel data; 31 | const PredictionUpdated(this.data); 32 | 33 | @override 34 | List get props => [data]; 35 | } 36 | 37 | class PredictionStopped extends PredictionState {} 38 | 39 | class PredictionCompleted extends PredictionState {} 40 | 41 | class PredictionsLoading extends PredictionState {} 42 | 43 | class PredictionsLoaded extends PredictionState { 44 | final List data; 45 | const PredictionsLoaded(this.data); 46 | 47 | @override 48 | List get props => [data]; 49 | } 50 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // import 'package:cloud_firestore/cloud_firestore.dart'; 2 | // import 'package:cloud_functions/cloud_functions.dart'; 3 | // import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:flutter_scribble/blocs/app_cubit.dart'; 7 | import 'package:flutter_scribble/firebase_options.dart'; 8 | import 'package:flutter_scribble/repository/prediction_repo.dart'; 9 | import 'package:flutter_scribble/services/backend_service.dart'; 10 | import 'package:flutter_scribble/ui/home/home_page.dart'; 11 | 12 | import 'package:firebase_core/firebase_core.dart'; 13 | import 'package:flutter_scribble/ui/result/result_page.dart'; 14 | import 'package:get_it/get_it.dart'; 15 | import 'package:go_router/go_router.dart'; 16 | 17 | final getIt = GetIt.instance; 18 | 19 | void main() async { 20 | WidgetsFlutterBinding.ensureInitialized(); 21 | 22 | await Firebase.initializeApp( 23 | options: DefaultFirebaseOptions.currentPlatform, 24 | ); 25 | 26 | await injectDependencies(); 27 | 28 | // if (kDebugMode) { 29 | // try { 30 | // FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080); 31 | // FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001); 32 | // } catch (e) { 33 | // // ignore: avoid_print 34 | // print(e); 35 | // } 36 | // } 37 | 38 | runApp( 39 | BlocProvider( 40 | create: (_) => AppCubit()..init(), 41 | child: const MyApp(), 42 | ), 43 | ); 44 | } 45 | 46 | Future injectDependencies() async { 47 | getIt.registerLazySingleton( 48 | () => PredictionRepositoryImpl()); 49 | getIt.registerLazySingleton(() => BackendServiceImpl()); 50 | } 51 | 52 | // GoRouter configuration 53 | final _router = GoRouter( 54 | routes: [ 55 | GoRoute( 56 | path: '/', 57 | builder: (context, state) => 58 | const HomePage(title: 'Scribble Diffusion with Flutter'), 59 | routes: [ 60 | GoRoute( 61 | path: 'share/:id', 62 | builder: (context, state) => 63 | ResultPage(id: state.params['id'] ?? ''), 64 | ) 65 | ]), 66 | ], 67 | ); 68 | 69 | class MyApp extends StatelessWidget { 70 | const MyApp({super.key}); 71 | 72 | // This widget is the root of your application. 73 | @override 74 | Widget build(BuildContext context) { 75 | return MaterialApp.router( 76 | title: '🐦🖼️ Flutter Scribble', 77 | theme: ThemeData( 78 | primaryColorDark: Colors.blueGrey, 79 | brightness: Brightness.dark, 80 | primarySwatch: Colors.indigo, 81 | scaffoldBackgroundColor: const Color(0xFF121212), 82 | ), 83 | routerConfig: _router, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/models/prediction_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class PredictionModel extends Equatable { 4 | final String inputImageUrl; 5 | final String outputImageUrl; 6 | final String prompt; 7 | final String id; 8 | 9 | const PredictionModel( 10 | {required this.id, 11 | required this.prompt, 12 | required this.inputImageUrl, 13 | required this.outputImageUrl}); 14 | 15 | static PredictionModel fromMap(Map data) { 16 | return PredictionModel( 17 | inputImageUrl: data['input'] ?? '', 18 | outputImageUrl: data['output'] ?? '', 19 | prompt: data['prompt'] ?? '', 20 | id: data['id'] ?? '', 21 | ); 22 | } 23 | 24 | Map toMap() { 25 | return { 26 | 'input': inputImageUrl, 27 | 'output': outputImageUrl, 28 | 'prompt': prompt, 29 | 'id': id, 30 | }; 31 | } 32 | 33 | @override 34 | String toString() { 35 | return 'PredictionModel{id: $id, inputImageUrl: $inputImageUrl, outputImageUrl: $outputImageUrl prompt: $prompt}'; 36 | } 37 | 38 | @override 39 | List get props => [id, inputImageUrl, outputImageUrl, prompt]; 40 | } 41 | -------------------------------------------------------------------------------- /lib/repository/prediction_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter_scribble/models/prediction_model.dart'; 4 | 5 | abstract class PredictionRepository { 6 | Stream onPredictionUpdated(String id); 7 | Future> getShowcase(String uid); 8 | Future getPrediction(String id); 9 | } 10 | 11 | class PredictionRepositoryImpl extends PredictionRepository { 12 | final _predictionsRef = FirebaseFirestore.instance.collection('results'); 13 | final _showcaseRef = FirebaseFirestore.instance.collection('showcase'); 14 | 15 | @override 16 | Future> getShowcase(String uid) async { 17 | List resultsList = []; 18 | try { 19 | final predictions = await _getUserPredictions(uid); 20 | final showcase = await _getShowcase(); 21 | for (var element in predictions.docs) { 22 | var data = element.data(); 23 | data['id'] = element.id; 24 | resultsList.add(PredictionModel.fromMap(data)); 25 | } 26 | for (var element in showcase.docs) { 27 | resultsList.add(PredictionModel.fromMap(element.data())); 28 | } 29 | return resultsList; 30 | } on FirebaseException catch (e) { 31 | if (kDebugMode) { 32 | print('Failed to fetch predicitons with error ${e.code}: ${e.message}'); 33 | } 34 | return resultsList; 35 | } catch (e) { 36 | throw Exception(e.toString()); 37 | } 38 | } 39 | 40 | Future>> _getShowcase() async { 41 | return await _showcaseRef.limit(3).get(); 42 | } 43 | 44 | Future>> _getUserPredictions( 45 | String uid) async { 46 | return await _predictionsRef.where('user', isEqualTo: uid).get(); 47 | } 48 | 49 | @override 50 | Stream onPredictionUpdated(String id) { 51 | return _predictionsRef.doc(id).snapshots().map((snapshot) { 52 | final data = snapshot.data() ?? {}; 53 | data['id'] = id; 54 | return PredictionModel.fromMap(data); 55 | }); 56 | } 57 | 58 | @override 59 | Future getPrediction(String id) async { 60 | PredictionModel? predictionModel; 61 | final docSnapshot = await _predictionsRef.doc(id).get(); 62 | if (docSnapshot.exists) { 63 | final data = docSnapshot.data() ?? {}; 64 | data['id'] = id; 65 | predictionModel = PredictionModel.fromMap(data); 66 | } 67 | return predictionModel; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/services/backend_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_functions/cloud_functions.dart'; 2 | import 'package:firebase_analytics/firebase_analytics.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | abstract class BackendService { 7 | Future createPrediction(String imageData, String prompt); 8 | Future signInAnonUser(); 9 | Future logEvent(String name, Map? parameters); 10 | } 11 | 12 | class BackendServiceImpl extends BackendService { 13 | UserCredential? _userCredential; 14 | 15 | @override 16 | Future createPrediction(String imageData, String prompt) async { 17 | var userCredential = await signInAnonUser(); 18 | final userId = userCredential?.user?.uid ?? ''; 19 | try { 20 | final result = await FirebaseFunctions.instance 21 | .httpsCallable('createPrediction') 22 | .call({'image': imageData, 'prompt': prompt, 'userId': userId}); 23 | if (kDebugMode) { 24 | print(result.data); 25 | } 26 | return result.data['data'] ?? ''; 27 | } on FirebaseFunctionsException catch (error) { 28 | if (kDebugMode) { 29 | print(error); 30 | } 31 | return null; 32 | } catch (e) { 33 | throw Exception(e.toString()); 34 | } 35 | } 36 | 37 | @override 38 | Future signInAnonUser() async { 39 | try { 40 | _userCredential ??= await FirebaseAuth.instance.signInAnonymously(); 41 | } on FirebaseAuthException catch (e) { 42 | switch (e.code) { 43 | case 'operation-not-allowed': 44 | if (kDebugMode) { 45 | print('Anonymous auth hasn\'t been enabled for this project.'); 46 | } 47 | break; 48 | default: 49 | if (kDebugMode) { 50 | print('Unknown error. $e'); 51 | } 52 | } 53 | } 54 | return _userCredential; 55 | } 56 | 57 | @override 58 | Future logEvent(String name, Map? parameters) async { 59 | await FirebaseAnalytics.instance 60 | .logEvent(name: name, parameters: parameters); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/draw/canvas.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:perfect_freehand/perfect_freehand.dart'; 3 | import 'stroke.dart'; 4 | import 'stroke_options.dart'; 5 | 6 | class Paper extends CustomPainter { 7 | final List lines; 8 | final StrokeOptions options; 9 | 10 | Paper({required this.lines, required this.options}); 11 | 12 | @override 13 | void paint(Canvas canvas, Size size) { 14 | Paint paint = Paint()..color = Colors.black; 15 | 16 | for (int i = 0; i < lines.length; ++i) { 17 | final outlinePoints = getStroke( 18 | lines[i].points, 19 | size: options.size, 20 | thinning: options.thinning, 21 | smoothing: options.smoothing, 22 | streamline: options.streamline, 23 | taperStart: options.taperStart, 24 | capStart: options.capStart, 25 | taperEnd: options.taperEnd, 26 | capEnd: options.capEnd, 27 | simulatePressure: options.simulatePressure, 28 | isComplete: options.isComplete, 29 | ); 30 | 31 | final path = Path(); 32 | 33 | if (outlinePoints.isEmpty) { 34 | return; 35 | } else if (outlinePoints.length < 2) { 36 | // If the path only has one line, draw a dot. 37 | path.addOval(Rect.fromCircle( 38 | center: Offset(outlinePoints[0].x, outlinePoints[0].y), radius: 1)); 39 | } else { 40 | // Otherwise, draw a line that connects each point with a curve. 41 | path.moveTo(outlinePoints[0].x, outlinePoints[0].y); 42 | 43 | for (int i = 1; i < outlinePoints.length - 1; ++i) { 44 | final p0 = outlinePoints[i]; 45 | final p1 = outlinePoints[i + 1]; 46 | path.quadraticBezierTo( 47 | p0.x, p0.y, (p0.x + p1.x) / 2, (p0.y + p1.y) / 2); 48 | } 49 | } 50 | 51 | final rect = Rect.fromLTWH(0, 0, size.width, size.height); 52 | canvas.clipRect(rect); 53 | 54 | canvas.drawPath(path, paint); 55 | } 56 | } 57 | 58 | @override 59 | bool shouldRepaint(Paper oldDelegate) { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/draw/drawing_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:ui' as ui; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_bloc/flutter_bloc.dart'; 8 | import 'package:flutter_scribble/blocs/app_cubit.dart'; 9 | import 'package:flutter_scribble/blocs/app_states.dart'; 10 | import 'package:perfect_freehand/perfect_freehand.dart'; 11 | 12 | import "canvas.dart"; 13 | import "stroke.dart"; 14 | import 'stroke_options.dart'; 15 | 16 | class DrawingPage extends StatefulWidget { 17 | final Function({bool drawing}) callback; 18 | 19 | const DrawingPage({Key? key, required this.callback}) : super(key: key); 20 | 21 | @override 22 | State createState() => _DrawingPageState(); 23 | } 24 | 25 | class _DrawingPageState extends State { 26 | List lines = [ 27 | const Stroke( 28 | [ 29 | Point(100, 100), 30 | Point(300, 300), 31 | Point(400, 300), 32 | Point(300, 20), 33 | Point(30, 20), 34 | Point(60, 150), 35 | ], 36 | ), 37 | ]; 38 | 39 | Stroke? line; 40 | 41 | StrokeOptions options = StrokeOptions(); 42 | 43 | StreamController currentLineStreamController = 44 | StreamController.broadcast(); 45 | 46 | StreamController> linesStreamController = 47 | StreamController>.broadcast(); 48 | 49 | Future clear() async { 50 | setState(() { 51 | lines = []; 52 | line = null; 53 | }); 54 | } 55 | 56 | Future undo() async { 57 | if (lines.isNotEmpty) { 58 | setState(() { 59 | lines.removeLast(); 60 | line = null; 61 | }); 62 | } 63 | } 64 | 65 | Future updateSizeOption(double size) async { 66 | setState(() { 67 | options.size = size; 68 | }); 69 | } 70 | 71 | Future image(BuildContext context) async { 72 | final image = await toImage(); 73 | final futureBytes = await image.toByteData(format: ui.ImageByteFormat.png); 74 | final pngBytes = futureBytes!.buffer.asUint8List(); 75 | 76 | showDialog( 77 | context: context, 78 | builder: (ctx) { 79 | return Dialog( 80 | elevation: 1, 81 | insetPadding: const EdgeInsets.all(32.0), 82 | shape: RoundedRectangleBorder( 83 | borderRadius: BorderRadius.circular(15.0), 84 | ), 85 | child: Padding( 86 | padding: const EdgeInsets.all(16.0), 87 | child: SizedBox( 88 | height: 500, 89 | width: 500, 90 | child: Image.memory(pngBytes), 91 | ), 92 | ), 93 | ); 94 | }, 95 | ); 96 | } 97 | 98 | Future upload(cubit, prompt) async { 99 | final image = await toImage(); 100 | final futureBytes = await image.toByteData(format: ui.ImageByteFormat.png); 101 | final pngBytes = futureBytes!.buffer.asUint8List(); 102 | final imageData = base64Encode(pngBytes); 103 | 104 | await cubit.createPrediction(imageData, prompt); 105 | } 106 | 107 | Future toImage() async { 108 | final ui.PictureRecorder recorder = ui.PictureRecorder(); 109 | 110 | final Canvas canvas = Canvas(recorder); 111 | 112 | final sketcher = Paper(lines: lines, options: options); 113 | const size = Size(500.0, 500.0); 114 | sketcher.paint(canvas, size); 115 | 116 | final ui.Picture picture = recorder.endRecording(); 117 | return picture.toImage(size.width.toInt(), size.height.toInt()); 118 | } 119 | 120 | void onPointerDown(PointerDownEvent details) { 121 | widget.callback(drawing: true); // set drawing state to true 122 | options = StrokeOptions( 123 | simulatePressure: details.kind != ui.PointerDeviceKind.stylus, 124 | ); 125 | 126 | final box = context.findRenderObject() as RenderBox; 127 | final offset = box.globalToLocal(details.position); 128 | late final Point point; 129 | if (details.kind == ui.PointerDeviceKind.stylus) { 130 | point = Point( 131 | offset.dx, 132 | offset.dy, 133 | (details.pressure - details.pressureMin) / 134 | (details.pressureMax - details.pressureMin), 135 | ); 136 | } else { 137 | point = Point(offset.dx, offset.dy); 138 | } 139 | final points = [point]; 140 | line = Stroke(points); 141 | currentLineStreamController.add(line!); 142 | } 143 | 144 | void onPointerMove(PointerMoveEvent details) { 145 | final box = context.findRenderObject() as RenderBox; 146 | final offset = box.globalToLocal(details.position); 147 | late final Point point; 148 | if (details.kind == ui.PointerDeviceKind.stylus) { 149 | point = Point( 150 | offset.dx, 151 | offset.dy, 152 | (details.pressure - details.pressureMin) / 153 | (details.pressureMax - details.pressureMin), 154 | ); 155 | } else { 156 | point = Point(offset.dx, offset.dy); 157 | } 158 | final points = [...line!.points, point]; 159 | line = Stroke(points); 160 | currentLineStreamController.add(line!); 161 | } 162 | 163 | void onPointerUp(PointerUpEvent details) { 164 | lines = List.from(lines)..add(line!); 165 | linesStreamController.add(lines); 166 | widget.callback(drawing: false); // reset drawing state 167 | } 168 | 169 | Widget buildCurrentPath(BuildContext context) { 170 | return Listener( 171 | onPointerDown: onPointerDown, 172 | onPointerMove: onPointerMove, 173 | onPointerUp: onPointerUp, 174 | child: RepaintBoundary( 175 | child: SizedBox( 176 | //color: Colors.transparent, 177 | width: double.infinity, 178 | height: double.infinity, 179 | child: StreamBuilder( 180 | stream: currentLineStreamController.stream, 181 | builder: (context, snapshot) { 182 | return CustomPaint( 183 | size: const Size(double.infinity, double.infinity), 184 | painter: Paper( 185 | lines: line == null ? [] : [line!], 186 | options: options, 187 | ), 188 | ); 189 | })), 190 | ), 191 | ); 192 | } 193 | 194 | Widget buildAllPaths(BuildContext context) { 195 | return RepaintBoundary( 196 | child: SizedBox( 197 | width: double.infinity, 198 | height: double.infinity, 199 | child: StreamBuilder>( 200 | stream: linesStreamController.stream, 201 | builder: (context, snapshot) { 202 | return CustomPaint( 203 | size: const Size(double.infinity, double.infinity), 204 | painter: Paper( 205 | lines: lines, 206 | options: options, 207 | ), 208 | ); 209 | }, 210 | ), 211 | ), 212 | ); 213 | } 214 | 215 | Widget buildToolbar() { 216 | return Positioned( 217 | bottom: 10.0, 218 | left: 10.0, 219 | child: Column( 220 | crossAxisAlignment: CrossAxisAlignment.center, 221 | mainAxisAlignment: MainAxisAlignment.start, 222 | children: [ 223 | buildClearButton(context), 224 | buildUndoButton(context), 225 | ])); 226 | } 227 | 228 | Widget buildUndoButton(BuildContext context) { 229 | return GestureDetector( 230 | onTap: () { 231 | undo(); 232 | }, 233 | child: Container( 234 | padding: const EdgeInsets.symmetric(vertical: 8.0), 235 | child: const CircleAvatar( 236 | child: Icon( 237 | Icons.replay, 238 | size: 20.0, 239 | color: Colors.white, 240 | )), 241 | ), 242 | ); 243 | } 244 | 245 | Widget buildClearButton(BuildContext context) { 246 | return GestureDetector( 247 | onTap: () { 248 | clear(); 249 | }, 250 | child: Container( 251 | padding: const EdgeInsets.symmetric(vertical: 8.0), 252 | child: const CircleAvatar( 253 | child: Icon( 254 | Icons.clear_outlined, 255 | size: 20.0, 256 | color: Colors.white, 257 | )), 258 | ), 259 | ); 260 | } 261 | 262 | @override 263 | Widget build(BuildContext context) { 264 | return BlocListener( 265 | listener: (context, state) async { 266 | if (state is PredictionTriggered) { 267 | if (kDebugMode) { 268 | print('PredictionTriggered'); 269 | } 270 | await upload(context.read(), state.prompt); 271 | } 272 | }, 273 | child: Scaffold( 274 | backgroundColor: Colors.white, 275 | body: Stack( 276 | children: [ 277 | buildAllPaths(context), 278 | buildCurrentPath(context), 279 | buildToolbar() 280 | ], 281 | ), 282 | ), 283 | ); 284 | } 285 | 286 | @override 287 | void dispose() { 288 | linesStreamController.close(); 289 | currentLineStreamController.close(); 290 | super.dispose(); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /lib/ui/draw/stroke.dart: -------------------------------------------------------------------------------- 1 | import 'package:perfect_freehand/perfect_freehand.dart'; 2 | 3 | class Stroke { 4 | final List points; 5 | 6 | const Stroke(this.points); 7 | 8 | Stroke.fromJson(Map json) : points = json['points']; 9 | 10 | Map toJson() => {'points': points}; 11 | } 12 | 13 | class SerializablePoint extends Point { 14 | const SerializablePoint( 15 | x, 16 | y, [ 17 | p = 0.5, 18 | ]) : super(x, y, p); 19 | 20 | SerializablePoint.fromJson(Map json) 21 | : super(json['x'], json['y'], json['p']); 22 | 23 | Map toJson() => {'x': 100}; 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/draw/stroke_options.dart: -------------------------------------------------------------------------------- 1 | class StrokeOptions { 2 | /// The base size (diameter) of the stroke. 3 | double size; 4 | 5 | /// The effect of pressure on the stroke's size. 6 | double thinning; 7 | 8 | /// Controls the density of points along the stroke's edges. 9 | double smoothing; 10 | 11 | /// Controls the level of variation allowed in the input points. 12 | double streamline; 13 | 14 | // Whether to simulate pressure or use the point's provided pressures. 15 | final bool simulatePressure; 16 | 17 | // The distance to taper the front of the stroke. 18 | double taperStart; 19 | 20 | // The distance to taper the end of the stroke. 21 | double taperEnd; 22 | 23 | // Whether to add a cap to the start of the stroke. 24 | final bool capStart; 25 | 26 | // Whether to add a cap to the end of the stroke. 27 | final bool capEnd; 28 | 29 | // Whether the line is complete. 30 | final bool isComplete; 31 | 32 | StrokeOptions({ 33 | this.size = 5, 34 | this.thinning = 0.7, 35 | this.smoothing = 0.5, 36 | this.streamline = 0.5, 37 | this.taperStart = 0.0, 38 | this.capStart = true, 39 | this.taperEnd = 0.0, 40 | this.capEnd = true, 41 | this.simulatePressure = true, 42 | this.isComplete = false, 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /lib/ui/home/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_linkify/flutter_linkify.dart'; 5 | import 'package:flutter_scribble/blocs/app_cubit.dart'; 6 | import 'package:flutter_scribble/blocs/app_states.dart'; 7 | import 'package:flutter_scribble/models/prediction_model.dart'; 8 | import 'package:flutter_scribble/repository/prediction_repo.dart'; 9 | import 'package:flutter_scribble/services/backend_service.dart'; 10 | import 'package:flutter_scribble/ui/draw/drawing_page.dart'; 11 | import 'package:get_it/get_it.dart'; 12 | import 'package:go_router/go_router.dart'; 13 | import 'package:loading_gifs/loading_gifs.dart'; 14 | import 'package:url_launcher/url_launcher.dart'; 15 | 16 | class HomePage extends StatefulWidget { 17 | const HomePage({super.key, required this.title}); 18 | 19 | final String title; 20 | 21 | @override 22 | State createState() => _HomePageState(); 23 | } 24 | 25 | class _HomePageState extends State { 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | // appBar: AppBar( 30 | // title: Text(widget.title), 31 | // backgroundColor: const Color.fromRGBO(32, 32, 36, 1), 32 | // ), 33 | body: Center( 34 | child: BlocListener( 35 | listener: (context, state) { 36 | if (state is PredictionStarted) { 37 | if (kDebugMode) { 38 | print('prediction started with id: ${state.id}'); 39 | } 40 | context.read().listenToPrediction(state.id); 41 | } 42 | }, 43 | child: const HomeList(), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | 50 | class HomeList extends StatefulWidget { 51 | const HomeList({super.key}); 52 | 53 | @override 54 | State createState() => _HomeListState(); 55 | } 56 | 57 | class _HomeListState extends State { 58 | bool _isDrawing = false; 59 | bool _isReadyForPrediction = true; 60 | bool _isWaitingForInitial = false; 61 | final PredictionRepository _predictionRepository = GetIt.instance(); 62 | final BackendService _backendService = GetIt.instance(); 63 | final List _results = []; 64 | 65 | final _promptController = 66 | TextEditingController(text: 'a futuristic spaceship in the galaxy'); 67 | 68 | @override 69 | void initState() { 70 | _getRecentResults(); 71 | super.initState(); 72 | } 73 | 74 | Future _getRecentResults() async { 75 | final user = await _backendService.signInAnonUser(); 76 | final results = 77 | await _predictionRepository.getShowcase(user?.user?.uid ?? ''); 78 | setState(() { 79 | _results.addAll(results); 80 | }); 81 | } 82 | 83 | @override 84 | void dispose() { 85 | _promptController.dispose(); 86 | super.dispose(); 87 | } 88 | 89 | void callback({drawing}) { 90 | setState(() { 91 | _isDrawing = drawing; 92 | }); 93 | } 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | return BlocListener( 98 | listener: (context, state) async { 99 | if (state is PredictionCompleted) { 100 | setState(() { 101 | _isReadyForPrediction = true; 102 | }); 103 | } 104 | if (state is PredictionUpdated) { 105 | setState(() { 106 | _isWaitingForInitial = false; 107 | final index = 108 | _results.indexWhere((element) => element.id == state.data.id); 109 | index == -1 110 | ? _results.insert(0, state.data) 111 | : _results[index] = state.data; 112 | }); 113 | if (kDebugMode) { 114 | print("Prediction Updated"); 115 | } 116 | } 117 | }, 118 | child: ListView( 119 | shrinkWrap: true, 120 | physics: _isDrawing 121 | ? const NeverScrollableScrollPhysics() 122 | : const AlwaysScrollableScrollPhysics(), 123 | padding: const EdgeInsets.all(15.0), 124 | children: [ 125 | const Align( 126 | child: Padding( 127 | padding: EdgeInsets.only(top: 15.0, bottom: 8.0), 128 | child: Text( 129 | 'Scribble Diffusion', 130 | style: TextStyle(fontSize: 40.0), 131 | ), 132 | )), 133 | const Align( 134 | child: Padding( 135 | padding: EdgeInsets.only(bottom: 10.0), 136 | child: Text( 137 | 'Turn your scribble to an image with AI', 138 | style: TextStyle(color: Colors.grey, fontSize: 25.0), 139 | textAlign: TextAlign.center, 140 | ), 141 | )), 142 | const Align( 143 | child: Padding( 144 | padding: EdgeInsets.only(bottom: 25.0), 145 | child: Text( 146 | 'Powered by ControlNet, Replicate, Flutter, and Firebase', 147 | style: TextStyle(color: Colors.grey), 148 | ), 149 | )), 150 | Align( 151 | child: Container( 152 | constraints: 153 | const BoxConstraints(maxHeight: 500.0, maxWidth: 500.0), 154 | child: AspectRatio( 155 | aspectRatio: 1, 156 | child: DrawingPage(callback: callback), 157 | ), 158 | ), 159 | ), 160 | const SizedBox(height: 10), 161 | Align( 162 | child: Container( 163 | constraints: const BoxConstraints(maxWidth: 500.0), 164 | child: Row( 165 | mainAxisAlignment: MainAxisAlignment.center, 166 | children: [ 167 | Expanded( 168 | flex: 7, 169 | child: TextField( 170 | controller: _promptController, 171 | decoration: const InputDecoration( 172 | border: OutlineInputBorder(), 173 | hintText: 'Type your prompt here', 174 | ), 175 | ), 176 | ), 177 | const SizedBox(width: 10), 178 | Expanded( 179 | flex: 3, 180 | child: ElevatedButton( 181 | style: ElevatedButton.styleFrom( 182 | minimumSize: const Size.fromHeight(55.0)), 183 | onPressed: _isReadyForPrediction 184 | ? () { 185 | if (_promptController.text.isEmpty) { 186 | return; 187 | } 188 | setState(() { 189 | _isReadyForPrediction = false; 190 | _isWaitingForInitial = true; 191 | }); 192 | context 193 | .read() 194 | .startPrediction(_promptController.text); 195 | } 196 | : null, 197 | child: const Icon(Icons.draw_outlined)), 198 | ) 199 | ], 200 | ), 201 | ), 202 | ), 203 | Column( 204 | children: _isWaitingForInitial 205 | ? const [ 206 | SizedBox(height: 10), 207 | Card( 208 | child: Center( 209 | child: Padding( 210 | padding: EdgeInsets.only(top: 108.0, bottom: 108.0), 211 | child: CircularProgressIndicator(), 212 | ), 213 | ), 214 | ), 215 | ] 216 | : [], 217 | ), 218 | Column( 219 | children: buildResults(), 220 | ), 221 | Align( 222 | child: SizedBox( 223 | width: double.infinity, 224 | child: Card( 225 | color: Colors.transparent, 226 | child: Center( 227 | child: Padding( 228 | padding: const EdgeInsets.all(20.0), 229 | child: Linkify( 230 | onOpen: _launchUrl, 231 | style: const TextStyle(color: Colors.grey), 232 | text: 233 | 'Powered by ControlNet, Replicate, Flutter, and Firebase. Code: https://github.com/lahirumaramba/flutter_scribble. 💬: https://twitter.com/lahiru. Inspired by https://scribblediffusion.com', 234 | ), 235 | ), 236 | ), 237 | ), 238 | ), 239 | ), 240 | ], 241 | ), 242 | ); 243 | } 244 | 245 | Future _launchUrl(LinkableElement linkableElement) async { 246 | final link = Uri.parse(linkableElement.url); 247 | if (!await launchUrl(link)) { 248 | throw Exception('Could not launch $link'); 249 | } 250 | } 251 | 252 | List buildResults() { 253 | var resultWidgets = []; 254 | resultWidgets.add( 255 | const SizedBox( 256 | height: 20, 257 | ), 258 | ); 259 | 260 | for (var prediction in _results) { 261 | resultWidgets.add( 262 | Card( 263 | child: Column( 264 | children: [ 265 | Row( 266 | mainAxisAlignment: MainAxisAlignment.center, 267 | children: [ 268 | Expanded( 269 | child: Padding( 270 | padding: const EdgeInsets.fromLTRB(20.0, 20.0, 8.0, 20.0), 271 | child: Container( 272 | color: const Color.fromARGB(200, 255, 255, 255), 273 | child: FadeInImage.assetNetwork( 274 | fit: BoxFit.contain, 275 | placeholder: circularProgressIndicatorSmall, 276 | image: prediction.inputImageUrl, 277 | placeholderScale: 5), 278 | ), 279 | ), 280 | ), 281 | Expanded( 282 | child: Padding( 283 | padding: const EdgeInsets.fromLTRB(8.0, 20.0, 20.0, 20.0), 284 | child: prediction.outputImageUrl.isEmpty 285 | ? Image.asset(circularProgressIndicatorSmall) 286 | : FadeInImage.assetNetwork( 287 | fit: BoxFit.contain, 288 | placeholder: circularProgressIndicatorSmall, 289 | image: prediction.outputImageUrl, 290 | placeholderScale: 5), 291 | ), 292 | ), 293 | ], 294 | ), 295 | Row( 296 | children: [ 297 | Padding( 298 | padding: const EdgeInsets.only(bottom: 20.0, left: 10.0), 299 | child: TextButton( 300 | style: 301 | TextButton.styleFrom(foregroundColor: Colors.white), 302 | onPressed: () => context.go('/share/${prediction.id}'), 303 | child: const Icon(Icons.link_outlined), 304 | ), 305 | ), 306 | Expanded( 307 | child: Padding( 308 | padding: const EdgeInsets.only(bottom: 20.0, left: 10.0), 309 | child: SizedBox( 310 | width: double.infinity, 311 | child: Text( 312 | prediction.prompt.toUpperCase(), 313 | ), 314 | ), 315 | ), 316 | ), 317 | ], 318 | ), 319 | ], 320 | ), 321 | ), 322 | ); 323 | } 324 | return resultWidgets; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /lib/ui/result/result_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_scribble/models/prediction_model.dart'; 3 | import 'package:flutter_scribble/repository/prediction_repo.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | import 'package:go_router/go_router.dart'; 6 | import 'package:loading_gifs/loading_gifs.dart'; 7 | 8 | class ResultPage extends StatefulWidget { 9 | const ResultPage({super.key, required this.id}); 10 | 11 | final String id; 12 | 13 | @override 14 | State createState() => _ResultPageState(); 15 | } 16 | 17 | class _ResultPageState extends State { 18 | final PredictionRepository _predictionRepository = GetIt.instance(); 19 | PredictionModel? _predictionModel; 20 | 21 | @override 22 | void initState() { 23 | _getPredicitonResult(); 24 | super.initState(); 25 | } 26 | 27 | void _getPredicitonResult() async { 28 | if (widget.id.isNotEmpty) { 29 | final result = await _predictionRepository.getPrediction(widget.id); 30 | setState(() { 31 | _predictionModel = result; 32 | }); 33 | } 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Scaffold( 39 | body: Column( 40 | children: [ 41 | Padding( 42 | padding: const EdgeInsets.only(top: 20.0, bottom: 10.0), 43 | child: ElevatedButton( 44 | onPressed: () => context.go('/'), 45 | child: const Text('Create Your Own'), 46 | ), 47 | ), 48 | Padding( 49 | padding: const EdgeInsets.all(8.0), 50 | child: Card( 51 | child: Column( 52 | mainAxisAlignment: MainAxisAlignment.center, 53 | children: _predictionModel == null 54 | ? [ 55 | const Center( 56 | child: Text( 57 | 'Uh oh, we could not find the results you were looking for.', 58 | ), 59 | ), 60 | ] 61 | : buildResults(), 62 | ), 63 | ), 64 | ), 65 | ], 66 | ), 67 | ); 68 | } 69 | 70 | List buildResults() { 71 | final prediction = _predictionModel!; 72 | return [ 73 | Row( 74 | mainAxisAlignment: MainAxisAlignment.center, 75 | children: [ 76 | Expanded( 77 | child: Padding( 78 | padding: const EdgeInsets.fromLTRB(20.0, 20.0, 8.0, 20.0), 79 | child: Container( 80 | color: const Color.fromARGB(200, 255, 255, 255), 81 | child: FadeInImage.assetNetwork( 82 | fit: BoxFit.contain, 83 | placeholder: circularProgressIndicatorSmall, 84 | image: prediction.inputImageUrl, 85 | placeholderScale: 5), 86 | ), 87 | ), 88 | ), 89 | Expanded( 90 | child: Padding( 91 | padding: const EdgeInsets.fromLTRB(8.0, 20.0, 20.0, 20.0), 92 | child: prediction.outputImageUrl.isEmpty 93 | ? Image.asset(circularProgressIndicatorSmall) 94 | : FadeInImage.assetNetwork( 95 | fit: BoxFit.contain, 96 | placeholder: circularProgressIndicatorSmall, 97 | image: prediction.outputImageUrl, 98 | placeholderScale: 5), 99 | ), 100 | ), 101 | ], 102 | ), 103 | Padding( 104 | padding: const EdgeInsets.only(bottom: 20.0, left: 10.0), 105 | child: Text(prediction.prompt.toUpperCase()), 106 | ), 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _flutterfire_internals: 5 | dependency: transitive 6 | description: 7 | name: _flutterfire_internals 8 | sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "1.0.16" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.10.0" 20 | bloc: 21 | dependency: transitive 22 | description: 23 | name: bloc 24 | sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "8.1.1" 28 | boolean_selector: 29 | dependency: transitive 30 | description: 31 | name: boolean_selector 32 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.1.1" 36 | characters: 37 | dependency: transitive 38 | description: 39 | name: characters 40 | sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.2.1" 44 | clock: 45 | dependency: transitive 46 | description: 47 | name: clock 48 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.1.1" 52 | cloud_firestore: 53 | dependency: "direct main" 54 | description: 55 | name: cloud_firestore 56 | sha256: f1e357be0ea277bfbb3347108573d0eaa7710adb07e9f86b1e24146355da6d40 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "4.4.2" 60 | cloud_firestore_platform_interface: 61 | dependency: transitive 62 | description: 63 | name: cloud_firestore_platform_interface 64 | sha256: "643eb0cb263af538882d5a42e8afa8f4756f8aa166c8854ea97f46143a904b38" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "5.11.2" 68 | cloud_firestore_web: 69 | dependency: transitive 70 | description: 71 | name: cloud_firestore_web 72 | sha256: "295fd862a55ecbeea9a336bf84ced901a838ee51a5866c24b706402d80cd1968" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.3.2" 76 | cloud_functions: 77 | dependency: "direct main" 78 | description: 79 | name: cloud_functions 80 | sha256: "2b96c69adfabd0c5c88b997c96ac7798fadd5e1ef5e440340edb793772aac10d" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "4.0.10" 84 | cloud_functions_platform_interface: 85 | dependency: transitive 86 | description: 87 | name: cloud_functions_platform_interface 88 | sha256: f02672abdaf372fd4a335a1695809d95d80a9e2a972d0c6887734d003f313fa9 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "5.1.29" 92 | cloud_functions_web: 93 | dependency: transitive 94 | description: 95 | name: cloud_functions_web 96 | sha256: f664fce1f2bc48c833c8949bbe507495ae9933c1552aa221edce1750b03d5a8f 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "4.3.18" 100 | collection: 101 | dependency: "direct main" 102 | description: 103 | name: collection 104 | sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.17.0" 108 | cupertino_icons: 109 | dependency: "direct main" 110 | description: 111 | name: cupertino_icons 112 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.0.5" 116 | equatable: 117 | dependency: "direct main" 118 | description: 119 | name: equatable 120 | sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.0.5" 124 | fake_async: 125 | dependency: transitive 126 | description: 127 | name: fake_async 128 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.3.1" 132 | firebase_analytics: 133 | dependency: "direct main" 134 | description: 135 | name: firebase_analytics 136 | sha256: "8b04353f985d35fd2b11192b94d742b167d0e735022942a4505ec27bc443f4b7" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "10.1.3" 140 | firebase_analytics_platform_interface: 141 | dependency: transitive 142 | description: 143 | name: firebase_analytics_platform_interface 144 | sha256: "5ea8d3f8dfa80c7f1ba854d6523884c7348815c4ab8dd0b0ade6b1e3886d90c6" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "3.3.20" 148 | firebase_analytics_web: 149 | dependency: transitive 150 | description: 151 | name: firebase_analytics_web 152 | sha256: d512fac6df28c33f639aded2380b7596d45fdc856b5fd9098d0ccd1718850bd6 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "0.5.1+11" 156 | firebase_app_check: 157 | dependency: "direct main" 158 | description: 159 | name: firebase_app_check 160 | sha256: "0e616815fe635335a80f8f79fe372337c416919b0ac53337102b8a4f464d8f51" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "0.1.1+11" 164 | firebase_app_check_platform_interface: 165 | dependency: transitive 166 | description: 167 | name: firebase_app_check_platform_interface 168 | sha256: f0437a008e28c1623b5455f7d2ff394cebd4d5a90e07a9cc82eefa68e9215078 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "0.0.5+14" 172 | firebase_app_check_web: 173 | dependency: transitive 174 | description: 175 | name: firebase_app_check_web 176 | sha256: "5082a633376ea8e5cdddf36359cd3a35befab9a52084c63d4af743051175df66" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "0.0.7+14" 180 | firebase_auth: 181 | dependency: "direct main" 182 | description: 183 | name: firebase_auth 184 | sha256: "9907d80446466e638dad31c195150b305dffd145dc57610fcd12c72289432143" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "4.2.9" 188 | firebase_auth_platform_interface: 189 | dependency: transitive 190 | description: 191 | name: firebase_auth_platform_interface 192 | sha256: c645fec50b0391aa878288f58fa4fe9762c271380c457aedf5c7c9b718604f68 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "6.11.11" 196 | firebase_auth_web: 197 | dependency: transitive 198 | description: 199 | name: firebase_auth_web 200 | sha256: "2dcf2a36852b9091741b4a4047a02e1f2c43a62c6cacec7df573a793a6543e6d" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "5.2.8" 204 | firebase_core: 205 | dependency: "direct main" 206 | description: 207 | name: firebase_core 208 | sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "2.7.0" 212 | firebase_core_platform_interface: 213 | dependency: transitive 214 | description: 215 | name: firebase_core_platform_interface 216 | sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "4.5.3" 220 | firebase_core_web: 221 | dependency: transitive 222 | description: 223 | name: firebase_core_web 224 | sha256: "291fbcace608aca6c860652e1358ef89752be8cc3ef227f8bbcd1e62775b833a" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "2.2.1" 228 | flutter: 229 | dependency: "direct main" 230 | description: flutter 231 | source: sdk 232 | version: "0.0.0" 233 | flutter_bloc: 234 | dependency: "direct main" 235 | description: 236 | name: flutter_bloc 237 | sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775" 238 | url: "https://pub.dev" 239 | source: hosted 240 | version: "8.1.2" 241 | flutter_linkify: 242 | dependency: "direct main" 243 | description: 244 | name: flutter_linkify 245 | sha256: c89fe74de985ec22f23d3538d2249add085a4f37ac1c29fd79e1a207efb81d63 246 | url: "https://pub.dev" 247 | source: hosted 248 | version: "5.0.2" 249 | flutter_lints: 250 | dependency: "direct dev" 251 | description: 252 | name: flutter_lints 253 | sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c 254 | url: "https://pub.dev" 255 | source: hosted 256 | version: "2.0.1" 257 | flutter_test: 258 | dependency: "direct dev" 259 | description: flutter 260 | source: sdk 261 | version: "0.0.0" 262 | flutter_web_plugins: 263 | dependency: transitive 264 | description: flutter 265 | source: sdk 266 | version: "0.0.0" 267 | get_it: 268 | dependency: "direct main" 269 | description: 270 | name: get_it 271 | sha256: "290fde3a86072e4b37dbb03c07bec6126f0ecc28dad403c12ffe2e5a2d751ab7" 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "7.2.0" 275 | go_router: 276 | dependency: "direct main" 277 | description: 278 | name: go_router 279 | sha256: b4bb06205ec607278b6fc23db238278417bca84a3905779cc68d1eb7afae37e2 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "6.2.0" 283 | http_parser: 284 | dependency: transitive 285 | description: 286 | name: http_parser 287 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "4.0.2" 291 | intl: 292 | dependency: transitive 293 | description: 294 | name: intl 295 | sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "0.17.0" 299 | js: 300 | dependency: transitive 301 | description: 302 | name: js 303 | sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "0.6.5" 307 | linkify: 308 | dependency: transitive 309 | description: 310 | name: linkify 311 | sha256: bdfbdafec6cdc9cd0ebb333a868cafc046714ad508e48be8095208c54691d959 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "4.1.0" 315 | lints: 316 | dependency: transitive 317 | description: 318 | name: lints 319 | sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "2.0.1" 323 | loading_gifs: 324 | dependency: "direct main" 325 | description: 326 | name: loading_gifs 327 | sha256: d3034b73c492ac5b5aaab7cb5a62b0c0f9f44342715f46b4ba0dbf50daead11c 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "0.3.0" 331 | logging: 332 | dependency: transitive 333 | description: 334 | name: logging 335 | sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "1.1.1" 339 | matcher: 340 | dependency: transitive 341 | description: 342 | name: matcher 343 | sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "0.12.13" 347 | material_color_utilities: 348 | dependency: transitive 349 | description: 350 | name: material_color_utilities 351 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "0.2.0" 355 | meta: 356 | dependency: transitive 357 | description: 358 | name: meta 359 | sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "1.8.0" 363 | nested: 364 | dependency: transitive 365 | description: 366 | name: nested 367 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "1.0.0" 371 | path: 372 | dependency: transitive 373 | description: 374 | name: path 375 | sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b 376 | url: "https://pub.dev" 377 | source: hosted 378 | version: "1.8.2" 379 | perfect_freehand: 380 | dependency: "direct main" 381 | description: 382 | name: perfect_freehand 383 | sha256: "77bfdd5efb223d120de5cc18c5d6e0b36a835e920521cfe67e281960bad44c9b" 384 | url: "https://pub.dev" 385 | source: hosted 386 | version: "1.0.4" 387 | plugin_platform_interface: 388 | dependency: transitive 389 | description: 390 | name: plugin_platform_interface 391 | sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a 392 | url: "https://pub.dev" 393 | source: hosted 394 | version: "2.1.3" 395 | provider: 396 | dependency: transitive 397 | description: 398 | name: provider 399 | sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "6.0.5" 403 | sky_engine: 404 | dependency: transitive 405 | description: flutter 406 | source: sdk 407 | version: "0.0.99" 408 | source_span: 409 | dependency: transitive 410 | description: 411 | name: source_span 412 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "1.9.1" 416 | stack_trace: 417 | dependency: transitive 418 | description: 419 | name: stack_trace 420 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "1.11.0" 424 | stream_channel: 425 | dependency: transitive 426 | description: 427 | name: stream_channel 428 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "2.1.1" 432 | string_scanner: 433 | dependency: transitive 434 | description: 435 | name: string_scanner 436 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "1.2.0" 440 | term_glyph: 441 | dependency: transitive 442 | description: 443 | name: term_glyph 444 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "1.2.1" 448 | test_api: 449 | dependency: transitive 450 | description: 451 | name: test_api 452 | sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "0.4.16" 456 | typed_data: 457 | dependency: transitive 458 | description: 459 | name: typed_data 460 | sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "1.3.1" 464 | url_launcher: 465 | dependency: "direct main" 466 | description: 467 | name: url_launcher 468 | sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "6.1.9" 472 | url_launcher_android: 473 | dependency: transitive 474 | description: 475 | name: url_launcher_android 476 | sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "6.0.23" 480 | url_launcher_ios: 481 | dependency: transitive 482 | description: 483 | name: url_launcher_ios 484 | sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "6.1.0" 488 | url_launcher_linux: 489 | dependency: transitive 490 | description: 491 | name: url_launcher_linux 492 | sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "3.0.2" 496 | url_launcher_macos: 497 | dependency: transitive 498 | description: 499 | name: url_launcher_macos 500 | sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "3.0.2" 504 | url_launcher_platform_interface: 505 | dependency: transitive 506 | description: 507 | name: url_launcher_platform_interface 508 | sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "2.1.1" 512 | url_launcher_web: 513 | dependency: transitive 514 | description: 515 | name: url_launcher_web 516 | sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "2.0.14" 520 | url_launcher_windows: 521 | dependency: transitive 522 | description: 523 | name: url_launcher_windows 524 | sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "3.0.3" 528 | vector_math: 529 | dependency: transitive 530 | description: 531 | name: vector_math 532 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "2.1.4" 536 | sdks: 537 | dart: ">=2.19.2 <3.0.0" 538 | flutter: ">=3.3.0" 539 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_scribble 2 | description: ControlNet Scribble Diffusion with Flutter. 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=2.19.2 <3.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.2 38 | perfect_freehand: ^1.0.4 39 | firebase_core: ^2.6.1 40 | firebase_app_check: ^0.1.1+11 41 | cloud_functions: ^4.0.10 42 | cloud_firestore: ^4.4.2 43 | flutter_bloc: ^8.1.2 44 | equatable: ^2.0.5 45 | get_it: ^7.2.0 46 | loading_gifs: ^0.3.0 47 | collection: ^1.17.0 48 | flutter_linkify: ^5.0.2 49 | url_launcher: ^6.1.9 50 | firebase_analytics: ^10.1.3 51 | firebase_auth: ^4.2.9 52 | go_router: ^6.2.0 53 | 54 | dev_dependencies: 55 | flutter_test: 56 | sdk: flutter 57 | 58 | # The "flutter_lints" package below contains a set of recommended lints to 59 | # encourage good coding practices. The lint set provided by the package is 60 | # activated in the `analysis_options.yaml` file located at the root of your 61 | # package. See that file for information about deactivating specific lint 62 | # rules and activating additional ones. 63 | flutter_lints: ^2.0.0 64 | 65 | # For information on the generic Dart part of this file, see the 66 | # following page: https://dart.dev/tools/pub/pubspec 67 | 68 | # The following section is specific to Flutter packages. 69 | flutter: 70 | 71 | # The following line ensures that the Material Icons font is 72 | # included with your application, so that you can use the icons in 73 | # the material Icons class. 74 | uses-material-design: true 75 | 76 | # To add assets to your application, add an assets section, like this: 77 | # assets: 78 | # - images/a_dot_burr.jpeg 79 | # - images/a_dot_ham.jpeg 80 | 81 | # An image asset can refer to one or more resolution-specific "variants", see 82 | # https://flutter.dev/assets-and-images/#resolution-aware 83 | 84 | # For details regarding adding assets from package dependencies, see 85 | # https://flutter.dev/assets-and-images/#from-packages 86 | 87 | # To add custom fonts to your application, add a fonts section here, 88 | # in this "flutter" section. Each entry in this list should have a 89 | # "family" key with the font family name, and a "fonts" key with a 90 | # list giving the asset and other descriptors for the font. For 91 | # example: 92 | # fonts: 93 | # - family: Schyler 94 | # fonts: 95 | # - asset: fonts/Schyler-Regular.ttf 96 | # - asset: fonts/Schyler-Italic.ttf 97 | # style: italic 98 | # - family: Trajan Pro 99 | # fonts: 100 | # - asset: fonts/TrajanPro.ttf 101 | # - asset: fonts/TrajanPro_Bold.ttf 102 | # weight: 700 103 | # 104 | # For details regarding fonts from package dependencies, 105 | # see https://flutter.dev/custom-fonts/#from-packages 106 | -------------------------------------------------------------------------------- /sample_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/sample_01.png -------------------------------------------------------------------------------- /sample_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/sample_02.png -------------------------------------------------------------------------------- /sample_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/sample_03.png -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read, write: if false; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lahirumaramba/flutter_scribble/13f96c24238453eb78d08c8f9ecf6dd207c017b9/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_scribble 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_scribble", 3 | "short_name": "flutter_scribble", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------