├── .github └── workflows │ └── main.yml ├── .gitignore ├── .metadata ├── .pubignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── assets │ │ └── list_example.md │ ├── list_example.dart │ ├── main.dart │ └── themes.dart ├── pubspec.lock ├── pubspec.yaml ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-512.png │ ├── index.html │ └── manifest.json └── windows │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake │ └── runner │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── resources │ └── app_icon.ico │ ├── run_loop.cpp │ ├── run_loop.h │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── lib ├── flutterbook.dart └── src │ ├── editor │ ├── editor.dart │ ├── providers │ │ ├── canvas_delegate.dart │ │ ├── device_preview_provider.dart │ │ ├── grid_provider.dart │ │ ├── pan_provider.dart │ │ ├── tab_provider.dart │ │ └── zoom_provider.dart │ └── ui │ │ ├── doc_component │ │ ├── code_expansion_section.dart │ │ ├── doc_code_sample.dart │ │ └── doc_component.dart │ │ ├── doc_markdown.dart │ │ ├── editor.dart │ │ ├── editor_bottom_bar.dart │ │ ├── editor_tabs.dart │ │ └── styled_icon_button.dart │ ├── flutterbook.dart │ ├── navigation │ ├── models │ │ └── organizers.dart │ ├── navigation.dart │ └── ui │ │ ├── navigation_panel.dart │ │ └── navigation_tiles.dart │ ├── routing │ ├── controls.dart │ ├── router.dart │ └── story_provider.dart │ ├── styled_widgets │ ├── animations.dart │ ├── highlighter.dart │ ├── smooth_scroll.dart │ ├── styled_scaffold.dart │ ├── styled_widgets.dart │ └── unfocuser.dart │ ├── theme_provider.dart │ └── utils │ ├── extensions.dart │ ├── flutter_book_theme.dart │ ├── radii.dart │ ├── ripple.dart │ ├── styles.dart │ └── utils.dart ├── pubspec.lock ├── pubspec.yaml └── test └── src └── navigation └── models └── organizers_test.dart /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Running CI for flutterbook 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | run-unit-tests: 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: subosito/flutter-action@v2 12 | - name: Get flutter packages 13 | run: flutter pub get 14 | - name: Run all tests 15 | run: flutter test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/ephemeral 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | 77 | .vscode/ 78 | 79 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: d79295af24c3ed621c33713ecda14ad196fd9c31 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | example/ios/.gitignore 2 | example/ios/Flutter/AppFrameworkInfo.plist 3 | example/ios/Flutter/Debug.xcconfig 4 | example/ios/Flutter/Release.xcconfig 5 | example/ios/Podfile 6 | example/ios/Runner.xcodeproj/project.pbxproj 7 | example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 8 | example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 9 | example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings 10 | example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | - Initial release. 4 | 5 | ## 0.1.3 6 | 7 | \* Header/branding is now swappable with another widget of choice 8 | 9 | \* Scaling a widget will now clip according to the Canvas 10 | 11 | \* Refactor to fit new naming scheme 12 | 13 | - More components have been documented 14 | 15 | ## 0.1.4 16 | 17 | - Fix navigation path from excluding the direct parent of the component state 18 | 19 | # 0.1.5 20 | 21 | - Ignore case during search 22 | 23 | - Dependency Updates 24 | 25 | # 0.1.6 26 | 27 | - Device Preview 28 | - Add Document Preview Page 29 | - Add Markdown capabilites for `ComponentState` 30 | - Fixed bug with Controls not working 31 | 32 | # 0.1.7 33 | 34 | - Add DartCodeView for code snippets 35 | - Add padding to the Docs tab 36 | - Add support for Multi Theming (3+ themes) 37 | - Add toggle for panning 38 | - Add Markdown capabilites for `Component` 39 | - Fixed bug in example project component 40 | - _BREAKING_ Renamed `DarkThemeProvider` to `ThemeProvider` 41 | 42 | # 0.1.8 43 | 44 | - Add Controls for Items 45 | 46 | # 0.1.9 47 | 48 | - Update dependencies 49 | - Export l10n-related properties 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Philip Vu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flutterbook 2 | 3 | # Flutterbook 4 | 5 | A storyboarding tool to accelerate the development of Flutter widgets. Inspired by Storybook.js. 6 | 7 | **The package is built to support Windows and Web layouts. There is no support for mobile support at the moment. This is new and still under heavy development, expect many changes to occur with usage.** 8 | 9 | License: MIT 10 | 11 | --- 12 | 13 | Go subscribe to my YouTube channel to support development. 14 | 15 | # [![MOONSDONTBURN Header](https://i.imgur.com/1QHjcUZ.png)](https://www.youtube.com/channel/UCurQRmT17EyOIrdPseiastg) 16 | 17 | ### Demo 18 | 19 | Demo Link Here 20 | 21 | ## Overview ✨ 22 | 23 | #### Creating the Main 24 | 25 | Start by creating the storyboard widget. This will wrap the Flutterbook widget 26 | so that when it is edited, it will hot reload. 27 | 28 | ```dart 29 | class Storyboard extends StatelessWidget { 30 | const Storyboard({Key? key}) : super(key: key); 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return FlutterBook( 35 | categories: [ 36 | // organizers ... 37 | ], 38 | ); 39 | } 40 | } 41 | ``` 42 | 43 | After creating the Storyboard widget, you can run it by using the main. I recommend making another 44 | file named `main_storyboard.dart` to maintain your storyboard. 45 | 46 | ```dart 47 | void main() { 48 | runApp(const Storyboard()); 49 | } 50 | // Or run it directly, but this will cost the hot reload. 51 | // void main() { 52 | // runApp(const Flutterbook(categories:[...])); 53 | // } 54 | ``` 55 | 56 | #### How to add Widgets 57 | 58 | To add widgets, you simply put `Organizers`, aka `Category`, `Folder`, `Component`, and `ComponentState`. 59 | 60 | The hierarchy goes 61 | 62 | `Category` > `Folder` > `Component` > `ComponentState` 63 | 64 | but you may also do 65 | 66 | `Category` > `Component` > `ComponentState` 67 | 68 | or 69 | 70 | `Category` > `Folder` > `Folder` > `Component` > `ComponentState` 71 | 72 | Here is a complex example of a single `Category`. In the example, we use the builder 73 | where we are able to access different controls. 74 | 75 | ```dart 76 | Category( 77 | categoryName: 'LIBRARY', 78 | organizers: [ 79 | Folder( 80 | folderName: 'Charts', 81 | organizers: [ 82 | Component( 83 | componentName: 'LineGraph', 84 | states: [ 85 | ComponentState.child( 86 | stateName: 'Default Container', 87 | child: Container(), 88 | ), 89 | ], 90 | ), 91 | ], 92 | ), 93 | Component( 94 | componentName: 'Button', 95 | compenentMarkdoown: """##A global button component""", 96 | states: [ 97 | ComponentState( 98 | stateName: 'Primary', 99 | markdown: """Used for the main action to be performed""" 100 | codeSample: r''' 101 | Button.primary( 102 | child: Text('Primary Button'), 103 | onPressed: () {}, 104 | ); 105 | ''' 106 | builder: (context, c) { 107 | return Center( 108 | child: SizedBox( 109 | width: c.number( 110 | label: 'Number', 111 | initial: 50, 112 | min: 50, 113 | max: 250, 114 | ), 115 | height: c.number( 116 | label: 'height', 117 | initial: 50, 118 | min: 50, 119 | max: 250, 120 | description: 'random stuff', 121 | ), 122 | child: CupertinoButton.filled( 123 | onPressed: c.boolean( 124 | label: 'boolean', 125 | initial: true, 126 | ) 127 | ? () {} 128 | : null, 129 | child: Text( 130 | c.text(label: 'Text', initial: 'Hello World'), 131 | ), 132 | ), 133 | ), 134 | ); 135 | }, 136 | ), 137 | ], 138 | ), 139 | ], 140 | ), 141 | ``` 142 | 143 | #### That's it! 144 | 145 | ## Roadmap 🚧 146 | 147 | - [ ] Mobile Support 148 | - [ ] 100% Code Coverage Testing 149 | - [X ] Documentation Support in ComponentState's 150 | - [ ] Better Docs 151 | - [X ] Device Previews 152 | - [ ] Shareable Handoffs 153 | - [ ] Optimization 154 | 155 | ## Contributors 🔥 156 | 157 | _Your name could be here_ 😉 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /analysis.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | omit_local_variable_types: false 6 | prefer_relative_imports: true 7 | prefer_null_aware_operators: true 8 | directives_ordering: true 9 | avoid_positional_boolean_parameters: false 10 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | **/ios 18 | .pubspec.lock 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | **/ios/**/Pods/ 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Web related 38 | lib/generated_plugin_registrant.dart 39 | 40 | # Symbolication related 41 | app.*.symbols 42 | 43 | # Obfuscation related 44 | app.*.map.json 45 | 46 | # Android Studio will place build artifacts here 47 | /android/app/debug 48 | /android/app/profile 49 | /android/app/release 50 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: d79295af24c3ed621c33713ecda14ad196fd9c31 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.example.example" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/assets/list_example.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # ListExample constructor 5 | 6 | 7 | 8 | 9 | *[](https://dart.dev/null-safety)* 10 | 11 | 12 | const 13 | ListExample({[Key](https://api.flutter.dev/flutter/foundation/Key-class.html)? key}) 14 | 15 | 16 |

I got a string

17 | 18 | 19 | 20 | ## Implementation 21 | 22 | ```dart 23 | const ListExample({Key? key}) : super(key: key); 24 | ``` 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/lib/list_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ListExample extends StatelessWidget { 4 | const ListExample({Key? key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | width: 500, 10 | color: Colors.white, 11 | child: ListView.builder( 12 | itemCount: 10, 13 | shrinkWrap: true, 14 | itemBuilder: (_, i) => Container( 15 | width: 400, 16 | height: 100, 17 | color: (i % 2 == 0) ? Colors.red : Colors.grey, 18 | ), 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/list_example.dart'; 2 | import 'package:example/themes.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutterbook/flutterbook.dart'; 6 | 7 | void main() { 8 | runApp(const Storyboard()); 9 | } 10 | 11 | class Storyboard extends StatelessWidget { 12 | const Storyboard({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return FlutterBook( 17 | themes: Themes.themes, 18 | categories: [ 19 | Category( 20 | categoryName: 'LIBRARY', 21 | 22 | organizers: [ 23 | Folder( 24 | folderName: 'Charts', 25 | organizers: [ 26 | Component( 27 | componentName: 'LineGraph', 28 | states: [ 29 | ComponentState.child( 30 | stateName: 'Default Container', 31 | child: Container(), 32 | ), 33 | ], 34 | ), 35 | ], 36 | ), 37 | Component( 38 | componentName: 'Button', 39 | componentMarkdown: """ 40 | ## Component Markdown Example 41 | The `Button` is global a component used for user actions. 42 | """, 43 | states: [ 44 | ComponentState( 45 | markdown: """ 46 | ### Component State Markdown Example 47 | The `Button.primary` is used for the main action to be performed. 48 | """, 49 | codeSample: r''' 50 | Button.primary( 51 | child: Text('Primary Button'), 52 | onPressed: () {}, 53 | ); 54 | ''', 55 | stateName: 'Primary', 56 | builder: (context, c) { 57 | return Center( 58 | child: SizedBox( 59 | width: c.number( 60 | label: 'Number', 61 | initial: 100, 62 | min: 100, 63 | max: 250, 64 | ), 65 | height: c.number( 66 | label: 'height', 67 | initial: 50, 68 | min: 50, 69 | max: 250, 70 | description: 'random stuff', 71 | ), 72 | child: CupertinoButton( 73 | color: c.list( 74 | label: "Color", 75 | initial: Colors.red, 76 | value: Colors.red, 77 | list: [ 78 | ListItem(title: "Red", value: Colors.red), 79 | ListItem(title: "Blue", value: Colors.blue), 80 | ListItem(title: "Black", value: Colors.black), 81 | ListItem(title: "Amber", value: Colors.amber), 82 | ], 83 | ), 84 | onPressed: c.boolean( 85 | label: 'boolean', 86 | initial: true, 87 | ) 88 | ? () {} 89 | : null, 90 | child: Text( 91 | c.text(label: 'Text', initial: 'Hello World'), 92 | ), 93 | ), 94 | ), 95 | ); 96 | }, 97 | ), 98 | ComponentState( 99 | markdown: """ 100 | ### Component State Markdown Example 101 | The `Button.secondary` is used for user alternate actions. 102 | """, 103 | codeSample: r''' 104 | Button.secondary( 105 | child: Text('Primary Button'), 106 | onPressed: () {}, 107 | ); 108 | ''', 109 | stateName: 'Secondary', 110 | builder: (context, c) { 111 | return Center( 112 | child: SizedBox( 113 | width: c.number( 114 | label: 'Number', 115 | initial: 100, 116 | min: 100, 117 | max: 250, 118 | ), 119 | height: c.number( 120 | label: 'height', 121 | initial: 50, 122 | min: 50, 123 | max: 250, 124 | description: 'random stuff', 125 | ), 126 | child: CupertinoButton( 127 | onPressed: c.boolean( 128 | label: 'boolean', 129 | initial: true, 130 | ) 131 | ? () {} 132 | : null, 133 | child: Text( 134 | c.text( 135 | label: 'Text', 136 | initial: 'Hello World', 137 | ), 138 | ), 139 | ), 140 | ), 141 | ); 142 | }, 143 | ), 144 | ], 145 | ), 146 | Component( 147 | componentName: 'List', 148 | states: [ 149 | ComponentState( 150 | codeSample: """ListExample()""", 151 | stateName: 'Primary', 152 | builder: (context, c) => ListExample(), 153 | ), 154 | ], 155 | ), 156 | ], 157 | 158 | ), 159 | ], 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /example/lib/themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterbook/flutterbook.dart'; 3 | 4 | class Themes { 5 | static List themes = [ 6 | FlutterBookTheme( 7 | theme: ThemeData( 8 | colorScheme: const ColorScheme.light( 9 | onSurface: Color(0xff222222), 10 | primary: Color(0xffce4a4f), 11 | primaryContainer: Color(0xffe07356), 12 | secondary: Color(0xff2b2540), 13 | secondaryContainer: Color(0xff483F6C), 14 | onSecondary: Colors.white, 15 | background: Color(0xfff3f6f9), 16 | onBackground: Color(0xff222222), 17 | ), 18 | dividerColor: const Color(0xff6C6F8D), 19 | canvasColor: const Color(0x7fc3e8f3), 20 | scaffoldBackgroundColor: const Color(0xfff3f6f9), 21 | visualDensity: VisualDensity.adaptivePlatformDensity, 22 | ), 23 | themeName: 'Light Theme', 24 | ), 25 | FlutterBookTheme( 26 | theme: ThemeData( 27 | colorScheme: const ColorScheme.dark( 28 | primary: Color(0xffce4a4f), 29 | onPrimary: Colors.white, 30 | secondary: Color(0xff917DD8), 31 | onSecondary: Colors.white, 32 | background: Color(0xff222222), 33 | onBackground: Color(0xfff3f6f9), 34 | ), 35 | hintColor: const Color(0xFFADADAD), 36 | dividerColor: const Color(0xff48445D), 37 | canvasColor: const Color(0x7f30393E), 38 | scaffoldBackgroundColor: const Color(0xff222222), 39 | visualDensity: VisualDensity.adaptivePlatformDensity, 40 | ), 41 | themeName: 'Dark Theme', 42 | ), 43 | FlutterBookTheme( 44 | theme: ThemeData( 45 | colorScheme: const ColorScheme.light( 46 | onSurface: Color(0xff222222), 47 | onPrimary: Colors.white, 48 | primary: Color.fromARGB(255, 206, 197, 74), 49 | primaryContainer: Color(0xffe07356), 50 | secondary: Color(0xff2b2540), 51 | secondaryContainer: Color(0xff483F6C), 52 | onSecondary: Colors.white, 53 | background: Color(0xfff3f6f9), 54 | onBackground: Color(0xff222222), 55 | ), 56 | dividerColor: const Color(0xff6C6F8D), 57 | canvasColor: const Color(0x7fc3e8f3), 58 | scaffoldBackgroundColor: Color.fromARGB(255, 244, 255, 150), 59 | visualDensity: VisualDensity.adaptivePlatformDensity, 60 | ), 61 | themeName: 'Sunny Theme', 62 | ), 63 | ]; 64 | } 65 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.17.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | flutterbook: 27 | path: .. 28 | 29 | 30 | # The following adds the Cupertino Icons font to your application. 31 | # Use with the CupertinoIcons class for iOS style icons. 32 | cupertino_icons: ^1.0.2 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | 38 | # For information on the generic Dart part of this file, see the 39 | # following page: https://dart.dev/tools/pub/pubspec 40 | 41 | # The following section is specific to Flutter. 42 | flutter: 43 | 44 | # The following line ensures that the Material Icons font is 45 | # included with your application, so that you can use the icons in 46 | # the material Icons class. 47 | uses-material-design: true 48 | 49 | # To add assets to your application, add an assets section, like this: 50 | assets: 51 | - lib/assets/list_example.md 52 | 53 | # An image asset can refer to one or more resolution-specific "variants", see 54 | # https://flutter.dev/assets-and-images/#resolution-aware. 55 | 56 | # For details regarding adding assets from package dependencies, see 57 | # https://flutter.dev/assets-and-images/#from-packages 58 | 59 | # To add custom fonts to your application, add a fonts section here, 60 | # in this "flutter" section. Each entry in this list should have a 61 | # "family" key with the font family name, and a "fonts" key with a 62 | # list giving the asset and other descriptors for the font. For 63 | # example: 64 | # fonts: 65 | # - family: Schyler 66 | # fonts: 67 | # - asset: fonts/Schyler-Regular.ttf 68 | # - asset: fonts/Schyler-Italic.ttf 69 | # style: italic 70 | # - family: Trajan Pro 71 | # fonts: 72 | # - asset: fonts/TrajanPro.ttf 73 | # - asset: fonts/TrajanPro_Bold.ttf 74 | # weight: 700 75 | # 76 | # For details regarding fonts from package dependencies, 77 | # see https://flutter.dev/custom-fonts/#from-packages 78 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | example 27 | 28 | 29 | 30 | 33 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 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 | } 24 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(example LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "example") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "run_loop.cpp" 8 | "utils.cpp" 9 | "win32_window.cpp" 10 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 11 | "Runner.rc" 12 | "runner.exe.manifest" 13 | ) 14 | apply_standard_settings(${BINARY_NAME}) 15 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 16 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 17 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 18 | add_dependencies(${BINARY_NAME} flutter_assemble) 19 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "A new Flutter project." "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "example" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "example.exe" "\0" 98 | VALUE "ProductName", "example" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(RunLoop* run_loop, 8 | const flutter::DartProject& project) 9 | : run_loop_(run_loop), project_(project) {} 10 | 11 | FlutterWindow::~FlutterWindow() {} 12 | 13 | bool FlutterWindow::OnCreate() { 14 | if (!Win32Window::OnCreate()) { 15 | return false; 16 | } 17 | 18 | RECT frame = GetClientArea(); 19 | 20 | // The size here must match the window dimensions to avoid unnecessary surface 21 | // creation / destruction in the startup path. 22 | flutter_controller_ = std::make_unique( 23 | frame.right - frame.left, frame.bottom - frame.top, project_); 24 | // Ensure that basic setup of the controller was successful. 25 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 26 | return false; 27 | } 28 | RegisterPlugins(flutter_controller_->engine()); 29 | run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); 30 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 31 | return true; 32 | } 33 | 34 | void FlutterWindow::OnDestroy() { 35 | if (flutter_controller_) { 36 | run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); 37 | flutter_controller_ = nullptr; 38 | } 39 | 40 | Win32Window::OnDestroy(); 41 | } 42 | 43 | LRESULT 44 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 45 | WPARAM const wparam, 46 | LPARAM const lparam) noexcept { 47 | // Give Flutter, including plugins, an opportunity to handle window messages. 48 | if (flutter_controller_) { 49 | std::optional result = 50 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 51 | lparam); 52 | if (result) { 53 | return *result; 54 | } 55 | } 56 | 57 | switch (message) { 58 | case WM_FONTCHANGE: 59 | flutter_controller_->engine()->ReloadSystemFonts(); 60 | break; 61 | } 62 | 63 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 64 | } 65 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "run_loop.h" 10 | #include "win32_window.h" 11 | 12 | // A window that does nothing but host a Flutter view. 13 | class FlutterWindow : public Win32Window { 14 | public: 15 | // Creates a new FlutterWindow driven by the |run_loop|, hosting a 16 | // Flutter view running |project|. 17 | explicit FlutterWindow(RunLoop* run_loop, 18 | const flutter::DartProject& project); 19 | virtual ~FlutterWindow(); 20 | 21 | protected: 22 | // Win32Window: 23 | bool OnCreate() override; 24 | void OnDestroy() override; 25 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 26 | LPARAM const lparam) noexcept override; 27 | 28 | private: 29 | // The run loop driving events for this window. 30 | RunLoop* run_loop_; 31 | 32 | // The project to run. 33 | flutter::DartProject project_; 34 | 35 | // The Flutter instance hosted by this window. 36 | std::unique_ptr flutter_controller_; 37 | }; 38 | 39 | #endif // RUNNER_FLUTTER_WINDOW_H_ 40 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "run_loop.h" 7 | #include "utils.h" 8 | 9 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 10 | _In_ wchar_t *command_line, _In_ int show_command) { 11 | // Attach to console when present (e.g., 'flutter run') or create a 12 | // new console when running with a debugger. 13 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 14 | CreateAndAttachConsole(); 15 | } 16 | 17 | // Initialize COM, so that it is available for use in the library and/or 18 | // plugins. 19 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 20 | 21 | RunLoop run_loop; 22 | 23 | flutter::DartProject project(L"data"); 24 | 25 | std::vector command_line_arguments = 26 | GetCommandLineArguments(); 27 | 28 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 29 | 30 | FlutterWindow window(&run_loop, project); 31 | Win32Window::Point origin(10, 10); 32 | Win32Window::Size size(1280, 720); 33 | if (!window.CreateAndShow(L"example", origin, size)) { 34 | return EXIT_FAILURE; 35 | } 36 | window.SetQuitOnClose(true); 37 | 38 | run_loop.Run(); 39 | 40 | ::CoUninitialize(); 41 | return EXIT_SUCCESS; 42 | } 43 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostWalker562/flutterbook/663ad1021c0ac9b4fb007b8b37ce0250c417782c/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /example/windows/runner/run_loop.cpp: -------------------------------------------------------------------------------- 1 | #include "run_loop.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | RunLoop::RunLoop() {} 8 | 9 | RunLoop::~RunLoop() {} 10 | 11 | void RunLoop::Run() { 12 | bool keep_running = true; 13 | TimePoint next_flutter_event_time = TimePoint::clock::now(); 14 | while (keep_running) { 15 | std::chrono::nanoseconds wait_duration = 16 | std::max(std::chrono::nanoseconds(0), 17 | next_flutter_event_time - TimePoint::clock::now()); 18 | ::MsgWaitForMultipleObjects( 19 | 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), 20 | QS_ALLINPUT); 21 | bool processed_events = false; 22 | MSG message; 23 | // All pending Windows messages must be processed; MsgWaitForMultipleObjects 24 | // won't return again for items left in the queue after PeekMessage. 25 | while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { 26 | processed_events = true; 27 | if (message.message == WM_QUIT) { 28 | keep_running = false; 29 | break; 30 | } 31 | ::TranslateMessage(&message); 32 | ::DispatchMessage(&message); 33 | // Allow Flutter to process messages each time a Windows message is 34 | // processed, to prevent starvation. 35 | next_flutter_event_time = 36 | std::min(next_flutter_event_time, ProcessFlutterMessages()); 37 | } 38 | // If the PeekMessage loop didn't run, process Flutter messages. 39 | if (!processed_events) { 40 | next_flutter_event_time = 41 | std::min(next_flutter_event_time, ProcessFlutterMessages()); 42 | } 43 | } 44 | } 45 | 46 | void RunLoop::RegisterFlutterInstance( 47 | flutter::FlutterEngine* flutter_instance) { 48 | flutter_instances_.insert(flutter_instance); 49 | } 50 | 51 | void RunLoop::UnregisterFlutterInstance( 52 | flutter::FlutterEngine* flutter_instance) { 53 | flutter_instances_.erase(flutter_instance); 54 | } 55 | 56 | RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { 57 | TimePoint next_event_time = TimePoint::max(); 58 | for (auto instance : flutter_instances_) { 59 | std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); 60 | if (wait_duration != std::chrono::nanoseconds::max()) { 61 | next_event_time = 62 | std::min(next_event_time, TimePoint::clock::now() + wait_duration); 63 | } 64 | } 65 | return next_event_time; 66 | } 67 | -------------------------------------------------------------------------------- /example/windows/runner/run_loop.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_RUN_LOOP_H_ 2 | #define RUNNER_RUN_LOOP_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | // A runloop that will service events for Flutter instances as well 10 | // as native messages. 11 | class RunLoop { 12 | public: 13 | RunLoop(); 14 | ~RunLoop(); 15 | 16 | // Prevent copying 17 | RunLoop(RunLoop const&) = delete; 18 | RunLoop& operator=(RunLoop const&) = delete; 19 | 20 | // Runs the run loop until the application quits. 21 | void Run(); 22 | 23 | // Registers the given Flutter instance for event servicing. 24 | void RegisterFlutterInstance( 25 | flutter::FlutterEngine* flutter_instance); 26 | 27 | // Unregisters the given Flutter instance from event servicing. 28 | void UnregisterFlutterInstance( 29 | flutter::FlutterEngine* flutter_instance); 30 | 31 | private: 32 | using TimePoint = std::chrono::steady_clock::time_point; 33 | 34 | // Processes all currently pending messages for registered Flutter instances. 35 | TimePoint ProcessFlutterMessages(); 36 | 37 | std::set flutter_instances_; 38 | }; 39 | 40 | #endif // RUNNER_RUN_LOOP_H_ 41 | -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | namespace { 8 | 9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 10 | 11 | // The number of Win32Window objects that currently exist. 12 | static int g_active_window_count = 0; 13 | 14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 15 | 16 | // Scale helper to convert logical scaler values to physical using passed in 17 | // scale factor 18 | int Scale(int source, double scale_factor) { 19 | return static_cast(source * scale_factor); 20 | } 21 | 22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 23 | // This API is only needed for PerMonitor V1 awareness mode. 24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 25 | HMODULE user32_module = LoadLibraryA("User32.dll"); 26 | if (!user32_module) { 27 | return; 28 | } 29 | auto enable_non_client_dpi_scaling = 30 | reinterpret_cast( 31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 32 | if (enable_non_client_dpi_scaling != nullptr) { 33 | enable_non_client_dpi_scaling(hwnd); 34 | FreeLibrary(user32_module); 35 | } 36 | } 37 | 38 | } // namespace 39 | 40 | // Manages the Win32Window's window class registration. 41 | class WindowClassRegistrar { 42 | public: 43 | ~WindowClassRegistrar() = default; 44 | 45 | // Returns the singleton registar instance. 46 | static WindowClassRegistrar* GetInstance() { 47 | if (!instance_) { 48 | instance_ = new WindowClassRegistrar(); 49 | } 50 | return instance_; 51 | } 52 | 53 | // Returns the name of the window class, registering the class if it hasn't 54 | // previously been registered. 55 | const wchar_t* GetWindowClass(); 56 | 57 | // Unregisters the window class. Should only be called if there are no 58 | // instances of the window. 59 | void UnregisterWindowClass(); 60 | 61 | private: 62 | WindowClassRegistrar() = default; 63 | 64 | static WindowClassRegistrar* instance_; 65 | 66 | bool class_registered_ = false; 67 | }; 68 | 69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 70 | 71 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 72 | if (!class_registered_) { 73 | WNDCLASS window_class{}; 74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 75 | window_class.lpszClassName = kWindowClassName; 76 | window_class.style = CS_HREDRAW | CS_VREDRAW; 77 | window_class.cbClsExtra = 0; 78 | window_class.cbWndExtra = 0; 79 | window_class.hInstance = GetModuleHandle(nullptr); 80 | window_class.hIcon = 81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 82 | window_class.hbrBackground = 0; 83 | window_class.lpszMenuName = nullptr; 84 | window_class.lpfnWndProc = Win32Window::WndProc; 85 | RegisterClass(&window_class); 86 | class_registered_ = true; 87 | } 88 | return kWindowClassName; 89 | } 90 | 91 | void WindowClassRegistrar::UnregisterWindowClass() { 92 | UnregisterClass(kWindowClassName, nullptr); 93 | class_registered_ = false; 94 | } 95 | 96 | Win32Window::Win32Window() { 97 | ++g_active_window_count; 98 | } 99 | 100 | Win32Window::~Win32Window() { 101 | --g_active_window_count; 102 | Destroy(); 103 | } 104 | 105 | bool Win32Window::CreateAndShow(const std::wstring& title, 106 | const Point& origin, 107 | const Size& size) { 108 | Destroy(); 109 | 110 | const wchar_t* window_class = 111 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 112 | 113 | const POINT target_point = {static_cast(origin.x), 114 | static_cast(origin.y)}; 115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 117 | double scale_factor = dpi / 96.0; 118 | 119 | HWND window = CreateWindow( 120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 123 | nullptr, nullptr, GetModuleHandle(nullptr), this); 124 | 125 | if (!window) { 126 | return false; 127 | } 128 | 129 | return OnCreate(); 130 | } 131 | 132 | // static 133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 134 | UINT const message, 135 | WPARAM const wparam, 136 | LPARAM const lparam) noexcept { 137 | if (message == WM_NCCREATE) { 138 | auto window_struct = reinterpret_cast(lparam); 139 | SetWindowLongPtr(window, GWLP_USERDATA, 140 | reinterpret_cast(window_struct->lpCreateParams)); 141 | 142 | auto that = static_cast(window_struct->lpCreateParams); 143 | EnableFullDpiSupportIfAvailable(window); 144 | that->window_handle_ = window; 145 | } else if (Win32Window* that = GetThisFromHandle(window)) { 146 | return that->MessageHandler(window, message, wparam, lparam); 147 | } 148 | 149 | return DefWindowProc(window, message, wparam, lparam); 150 | } 151 | 152 | LRESULT 153 | Win32Window::MessageHandler(HWND hwnd, 154 | UINT const message, 155 | WPARAM const wparam, 156 | LPARAM const lparam) noexcept { 157 | switch (message) { 158 | case WM_DESTROY: 159 | window_handle_ = nullptr; 160 | Destroy(); 161 | if (quit_on_close_) { 162 | PostQuitMessage(0); 163 | } 164 | return 0; 165 | 166 | case WM_DPICHANGED: { 167 | auto newRectSize = reinterpret_cast(lparam); 168 | LONG newWidth = newRectSize->right - newRectSize->left; 169 | LONG newHeight = newRectSize->bottom - newRectSize->top; 170 | 171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 173 | 174 | return 0; 175 | } 176 | case WM_SIZE: { 177 | RECT rect = GetClientArea(); 178 | if (child_content_ != nullptr) { 179 | // Size and position the child window. 180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 181 | rect.bottom - rect.top, TRUE); 182 | } 183 | return 0; 184 | } 185 | 186 | case WM_ACTIVATE: 187 | if (child_content_ != nullptr) { 188 | SetFocus(child_content_); 189 | } 190 | return 0; 191 | } 192 | 193 | return DefWindowProc(window_handle_, message, wparam, lparam); 194 | } 195 | 196 | void Win32Window::Destroy() { 197 | OnDestroy(); 198 | 199 | if (window_handle_) { 200 | DestroyWindow(window_handle_); 201 | window_handle_ = nullptr; 202 | } 203 | if (g_active_window_count == 0) { 204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 205 | } 206 | } 207 | 208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 209 | return reinterpret_cast( 210 | GetWindowLongPtr(window, GWLP_USERDATA)); 211 | } 212 | 213 | void Win32Window::SetChildContent(HWND content) { 214 | child_content_ = content; 215 | SetParent(content, window_handle_); 216 | RECT frame = GetClientArea(); 217 | 218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 219 | frame.bottom - frame.top, true); 220 | 221 | SetFocus(child_content_); 222 | } 223 | 224 | RECT Win32Window::GetClientArea() { 225 | RECT frame; 226 | GetClientRect(window_handle_, &frame); 227 | return frame; 228 | } 229 | 230 | HWND Win32Window::GetHandle() { 231 | return window_handle_; 232 | } 233 | 234 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 235 | quit_on_close_ = quit_on_close; 236 | } 237 | 238 | bool Win32Window::OnCreate() { 239 | // No-op; provided for subclasses. 240 | return true; 241 | } 242 | 243 | void Win32Window::OnDestroy() { 244 | // No-op; provided for subclasses. 245 | } 246 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /lib/flutterbook.dart: -------------------------------------------------------------------------------- 1 | library flutterbook; 2 | 3 | export 'src/flutterbook.dart'; 4 | export 'src/theme_provider.dart'; 5 | export 'src/navigation/models/organizers.dart'; 6 | export 'src/utils/flutter_book_theme.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/editor/editor.dart: -------------------------------------------------------------------------------- 1 | export 'providers/canvas_delegate.dart'; 2 | export 'providers/grid_provider.dart'; 3 | export 'providers/zoom_provider.dart'; 4 | export 'ui/editor.dart'; 5 | export 'ui/editor_tabs.dart'; 6 | export 'ui/doc_component/doc_code_sample.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/editor/providers/canvas_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../routing/story_provider.dart'; 3 | 4 | class CanvasDelegateProvider extends ChangeNotifier { 5 | CanvasDelegateProvider({ 6 | StoryProvider? storyProvider, 7 | }) : _storyProvider = storyProvider; 8 | 9 | StoryProvider? _storyProvider; 10 | 11 | // ignore: unnecessary_getters_setters 12 | StoryProvider? get storyProvider => _storyProvider; 13 | 14 | set storyProvider(StoryProvider? provider) { 15 | _storyProvider = provider; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/editor/providers/device_preview_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DevicePreviewProvider extends ChangeNotifier { 4 | DevicePreviewProvider(); 5 | 6 | bool _show = false; 7 | 8 | get show => _show; 9 | 10 | void togglePreview() { 11 | _show = !_show; 12 | notifyListeners(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/editor/providers/grid_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridProvider extends ChangeNotifier { 4 | GridProvider(); 5 | 6 | bool _hasGrid = false; 7 | 8 | bool get grid => _hasGrid; 9 | 10 | void toggleGrid() { 11 | _hasGrid = !_hasGrid; 12 | notifyListeners(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/editor/providers/pan_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PanProvider extends ChangeNotifier { 4 | PanProvider(); 5 | 6 | bool _panEnabled = false; 7 | 8 | get panEnabled => _panEnabled; 9 | 10 | void toggle() { 11 | _panEnabled = !_panEnabled; 12 | notifyListeners(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/editor/providers/tab_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterbook/src/editor/ui/editor_tabs.dart'; 3 | 4 | class TabProvider extends ChangeNotifier { 5 | TabProvider(); 6 | FlutterBookTab _tab = FlutterBookTab.canvas; 7 | 8 | FlutterBookTab get tab => _tab; 9 | 10 | setTab(selectedTab) { 11 | _tab = selectedTab; 12 | notifyListeners(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/editor/providers/zoom_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ZoomProvider extends ChangeNotifier { 4 | ZoomProvider(); 5 | 6 | double _zoom = 1; 7 | 8 | double get zoom => _zoom; 9 | 10 | void zoomIn() { 11 | _zoom += 0.25; 12 | notifyListeners(); 13 | } 14 | 15 | void zoomOut() { 16 | _zoom = (_zoom - 0.25).clamp(0.5, 999); 17 | notifyListeners(); 18 | } 19 | 20 | void resetZoom() { 21 | _zoom = 1; 22 | notifyListeners(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/editor/ui/doc_component/code_expansion_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterbook/src/editor/ui/doc_component/doc_code_sample.dart'; 3 | import 'package:flutterbook/src/utils/utils.dart'; 4 | 5 | class CodeExpansionSection extends StatefulWidget { 6 | final String? code; 7 | 8 | CodeExpansionSection({Key? key, this.code}) : super(key: key); 9 | 10 | @override 11 | State createState() => CodeExpansionSectionState(); 12 | } 13 | 14 | class CodeExpansionSectionState extends State { 15 | bool expanded = false; 16 | 17 | bool get noCodeAvailable => widget.code == null; 18 | 19 | void toggleExpansion() { 20 | setState(() { 21 | expanded = !expanded; 22 | }); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return noCodeAvailable 28 | ? _NodeCodeContainer() 29 | : _CodeSection( 30 | code: widget.code!, 31 | isExpanded: expanded, 32 | onToggle: (_) => toggleExpansion(), 33 | ); 34 | } 35 | } 36 | 37 | class _NodeCodeContainer extends StatelessWidget { 38 | const _NodeCodeContainer({Key? key}) : super(key: key); 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Align( 43 | alignment: Alignment.centerRight, 44 | child: Container( 45 | decoration: BoxDecoration( 46 | borderRadius: BorderRadius.only( 47 | bottomRight: canvasRadius, 48 | ), 49 | color: Theme.of(context).shadowColor, 50 | ), 51 | padding: EdgeInsets.all(10), 52 | child: Text('No Code Available'), 53 | ), 54 | ); 55 | } 56 | } 57 | 58 | class _CodeSection extends StatelessWidget { 59 | final String code; 60 | final bool isExpanded; 61 | final Function(bool) onToggle; 62 | 63 | const _CodeSection({ 64 | Key? key, 65 | required this.code, 66 | required this.isExpanded, 67 | required this.onToggle, 68 | }) : super(key: key); 69 | 70 | @override 71 | Widget build(BuildContext context) { 72 | return Theme( 73 | data: Theme.of(context).copyWith(dividerColor: Colors.transparent), 74 | child: ExpansionTile( 75 | onExpansionChanged: onToggle, 76 | title: Text(isExpanded ? "Hide Code" : "Show Code"), 77 | children: [ 78 | if (isExpanded) 79 | Container( 80 | height: 400, 81 | child: DocCodeSample(code: code), 82 | ) 83 | else 84 | SizedBox.shrink() 85 | ], 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/editor/ui/doc_component/doc_code_sample.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_code_viewer2/dart_code_viewer2.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | final String noCodeProvided = r'''/// No code provided'''; 6 | 7 | class CodeSampleThemeData { 8 | final TextStyle? baseStyle; 9 | final TextStyle? classStyle; 10 | final TextStyle? commentStyle; 11 | final TextStyle? constantStyle; 12 | final TextStyle? keywordStyle; 13 | final TextStyle? numberStyle; 14 | final TextStyle? punctuationStyle; 15 | final TextStyle? stringStyle; 16 | final Color? backgroundColor; 17 | 18 | CodeSampleThemeData({ 19 | this.baseStyle, 20 | this.classStyle, 21 | this.commentStyle, 22 | this.constantStyle, 23 | this.keywordStyle, 24 | this.numberStyle, 25 | this.punctuationStyle, 26 | this.stringStyle, 27 | this.backgroundColor, 28 | }); 29 | } 30 | 31 | class DocCodeSample extends StatelessWidget { 32 | final String? code; 33 | final CodeSampleThemeData? themeData; 34 | 35 | const DocCodeSample({ 36 | Key? key, 37 | this.code, 38 | this.themeData, 39 | }) : super(key: key); 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return Tooltip( 44 | message: "Copy Code Snippet", 45 | child: GestureDetector( 46 | onTap: () { 47 | Clipboard.setData(new ClipboardData(text: code ?? '')); 48 | ScaffoldMessenger.of(context).showSnackBar( 49 | SnackBar( 50 | content: Text("Code copied to clipboard"), 51 | ), 52 | ); 53 | }, 54 | child: DartCodeViewer( 55 | '''\n${code ?? noCodeProvided}''', 56 | showCopyButton: false, 57 | baseStyle: themeData?.baseStyle, 58 | classStyle: themeData?.classStyle, 59 | commentStyle: themeData?.commentStyle, 60 | constantStyle: themeData?.constantStyle, 61 | keywordStyle: themeData?.keywordStyle, 62 | numberStyle: themeData?.numberStyle, 63 | punctuationStyle: themeData?.punctuationStyle, 64 | stringStyle: themeData?.stringStyle, 65 | backgroundColor: themeData?.backgroundColor, 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/editor/ui/doc_component/doc_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_feather_icons/flutter_feather_icons.dart'; 3 | import 'package:flutterbook/src/editor/ui/doc_component/code_expansion_section.dart'; 4 | import 'package:flutterbook/src/editor/ui/doc_markdown.dart'; 5 | import 'package:flutterbook/src/editor/ui/styled_icon_button.dart'; 6 | import 'package:flutterbook/src/utils/extensions.dart'; 7 | import 'package:flutterbook/src/utils/radii.dart'; 8 | 9 | class DocPanel extends StatelessWidget { 10 | final Widget component; 11 | final String? markdown; 12 | final String? codeSample; 13 | final String stateName; 14 | 15 | const DocPanel({ 16 | Key? key, 17 | this.codeSample, 18 | required this.component, 19 | this.markdown, 20 | required this.stateName, 21 | }) : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final TextStyle titleStyle = 26 | Theme.of(context).textTheme.titleMedium!.copyWith( 27 | color: Theme.of(context).hintColor, 28 | fontWeight: FontWeight.bold, 29 | fontSize: 16, 30 | ); 31 | 32 | return Column( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | Padding( 36 | padding: EdgeInsets.symmetric(vertical: 20), 37 | child: Text(stateName, style: titleStyle), 38 | ), 39 | CanvasPreview( 40 | component: component, 41 | codeSample: codeSample, 42 | ), 43 | if (markdown != null) 44 | DocMarkDown( 45 | markdown: markdown, 46 | ), 47 | ], 48 | ); 49 | } 50 | } 51 | 52 | class CanvasPreview extends StatefulWidget { 53 | final Widget component; 54 | final String? codeSample; 55 | 56 | const CanvasPreview({ 57 | Key? key, 58 | this.codeSample, 59 | required this.component, 60 | }) : super(key: key); 61 | 62 | @override 63 | State createState() => _CanvasPreviewState(); 64 | } 65 | 66 | class _CanvasPreviewState extends State { 67 | double _zoom = 1; 68 | get zoom => _zoom; 69 | 70 | void zoomIn() { 71 | setState(() { 72 | _zoom += 0.25; 73 | }); 74 | } 75 | 76 | void zoomOut() { 77 | setState(() { 78 | _zoom = (_zoom - 0.25).clamp(0.5, 999); 79 | }); 80 | } 81 | 82 | void resetZoom() { 83 | setState(() { 84 | _zoom = 1; 85 | }); 86 | } 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | TransformationController _transformation = TransformationController(); 91 | _transformation.value = Matrix4.identity()..scale(zoom); 92 | return Container( 93 | decoration: BoxDecoration( 94 | borderRadius: canvasBorderRadius, 95 | boxShadow: [ 96 | BoxShadow( 97 | blurRadius: 8, 98 | color: context.theme.shadowColor.withOpacity(0.075), 99 | ), 100 | ], 101 | color: context.colorScheme.surface, 102 | ), 103 | margin: const EdgeInsets.fromLTRB(0, 12, 12, 12), 104 | child: Column( 105 | crossAxisAlignment: CrossAxisAlignment.stretch, 106 | children: [ 107 | Row( 108 | children: [ 109 | VerticalDivider(), 110 | StyledTextButton( 111 | icon: FeatherIcons.zoomIn, 112 | onPressed: zoomIn, 113 | ), 114 | const SizedBox(width: 8), 115 | StyledTextButton( 116 | icon: FeatherIcons.zoomOut, 117 | onPressed: zoomOut, 118 | ), 119 | const SizedBox(width: 8), 120 | StyledTextButton( 121 | icon: FeatherIcons.refreshCcw, 122 | onPressed: resetZoom, 123 | ), 124 | ], 125 | ), 126 | Divider( 127 | color: context.theme.dividerColor.withOpacity(0.5), 128 | height: 0, 129 | ), 130 | Padding( 131 | padding: EdgeInsets.all(5), 132 | child: InteractiveViewer( 133 | boundaryMargin: EdgeInsets.all(double.infinity), 134 | panEnabled: true, 135 | transformationController: _transformation, 136 | child: widget.component, 137 | ), 138 | ), 139 | CodeExpansionSection(code: widget.codeSample), 140 | ], 141 | ), 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/editor/ui/doc_markdown.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_markdown/flutter_markdown.dart'; 3 | 4 | const defaultMarkDown = """ 5 | ## No markdown provided 6 | """; 7 | 8 | class DocMarkDown extends StatelessWidget { 9 | final String? markdown; 10 | 11 | const DocMarkDown({ 12 | Key? key, 13 | this.markdown, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Markdown( 19 | styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)), 20 | padding: EdgeInsets.only(top: 8), 21 | data: markdown ?? defaultMarkDown, 22 | selectable: true, 23 | shrinkWrap: true, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/editor/ui/editor.dart: -------------------------------------------------------------------------------- 1 | import 'package:device_preview/device_preview.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutterbook/src/editor/providers/device_preview_provider.dart'; 4 | import 'package:flutterbook/src/editor/providers/pan_provider.dart'; 5 | import 'package:flutterbook/src/editor/providers/tab_provider.dart'; 6 | import 'package:flutterbook/src/editor/ui/doc_component/doc_component.dart'; 7 | import 'package:flutterbook/src/editor/ui/doc_markdown.dart'; 8 | import 'package:flutterbook/src/routing/router.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | import '../../../flutterbook.dart'; 12 | import '../../routing/story_provider.dart'; 13 | import '../../utils/utils.dart'; 14 | import '../providers/canvas_delegate.dart'; 15 | import '../providers/grid_provider.dart'; 16 | import '../providers/zoom_provider.dart'; 17 | import 'editor_bottom_bar.dart'; 18 | import 'editor_tabs.dart' as editor; 19 | 20 | class Editor extends StatelessWidget { 21 | const Editor({Key? key, required this.component}) : super(key: key); 22 | 23 | final Widget? component; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | ComponentState? currentStory = 28 | context.watch().storyProvider?.currentStory; 29 | 30 | return Container( 31 | decoration: BoxDecoration( 32 | boxShadow: [ 33 | BoxShadow( 34 | blurRadius: 8, 35 | color: context.theme.shadowColor.withOpacity(0.075), 36 | ), 37 | ], 38 | borderRadius: canvasBorderRadius, 39 | color: context.colorScheme.surface, 40 | ), 41 | margin: const EdgeInsets.fromLTRB(0, 12, 12, 12), 42 | child: ClipRRect( 43 | borderRadius: canvasBorderRadius, 44 | child: Column( 45 | crossAxisAlignment: CrossAxisAlignment.stretch, 46 | children: [ 47 | SingleChildScrollView( 48 | scrollDirection: Axis.horizontal, 49 | child: editor.CoreContentTabs(), 50 | ), 51 | Divider( 52 | height: 0, 53 | color: context.theme.dividerColor.withOpacity(0.5), 54 | ), 55 | // Canvas 56 | Expanded( 57 | child: ClipRect( 58 | child: Stack( 59 | children: [ 60 | Positioned.fill( 61 | child: Consumer( 62 | builder: (context, model, child) { 63 | return model.grid 64 | ? GridPaper( 65 | color: context.theme.canvasColor, 66 | ) 67 | : const SizedBox.shrink(); 68 | }, 69 | ), 70 | ), 71 | Consumer( 72 | builder: (context, model, child) { 73 | List state = recursiveRetrievalOfStates( 74 | context.read>()); 75 | 76 | Widget element = 77 | model.tab == editor.FlutterBookTab.canvas 78 | ? _Canvas(component) 79 | : _Doc(state, currentStory); 80 | return element; 81 | }, 82 | ) 83 | ], 84 | ), 85 | ), 86 | ), 87 | Divider( 88 | height: 0, 89 | color: context.theme.dividerColor.withOpacity(0.5), 90 | ), 91 | const CoreBottomBar(), 92 | ], 93 | ), 94 | ), 95 | ); 96 | } 97 | } 98 | 99 | class _Canvas extends StatelessWidget { 100 | final Widget? component; 101 | _Canvas(this.component); 102 | 103 | @override 104 | Widget build(BuildContext context) { 105 | return Consumer( 106 | builder: (context, model, child) { 107 | TransformationController _transformation = TransformationController(); 108 | _transformation.value = Matrix4.identity()..scale(model.zoom); 109 | return context.watch().show 110 | ? _DevicePreviewCanvas(component) 111 | : _InteractiveViewerCanvas(component, _transformation); 112 | }, 113 | ); 114 | } 115 | } 116 | 117 | class _DevicePreviewCanvas extends StatelessWidget { 118 | final Widget? component; 119 | _DevicePreviewCanvas( 120 | this.component, 121 | ); 122 | @override 123 | Widget build(BuildContext context) { 124 | return DevicePreview( 125 | builder: (context) { 126 | return component ?? Container(); 127 | }, 128 | ); 129 | } 130 | } 131 | 132 | class _Doc extends StatelessWidget { 133 | final ComponentState? currentState; 134 | final List states; 135 | 136 | _Doc(this.states, this.currentState); 137 | 138 | @override 139 | Widget build(BuildContext context) { 140 | final TextStyle titleStyle = 141 | Theme.of(context).textTheme.titleMedium!.copyWith( 142 | color: Theme.of(context).hintColor, 143 | fontWeight: FontWeight.bold, 144 | fontSize: 32, 145 | ); 146 | 147 | return SingleChildScrollView( 148 | child: Align( 149 | alignment: Alignment.topCenter, 150 | child: Container( 151 | padding: EdgeInsets.symmetric(vertical: 64), 152 | constraints: BoxConstraints(maxWidth: 900), 153 | child: Column( 154 | mainAxisSize: MainAxisSize.min, 155 | crossAxisAlignment: CrossAxisAlignment.start, 156 | children: [ 157 | Text( 158 | currentState?.parent?.componentName ?? '', 159 | style: titleStyle, 160 | ), 161 | if (currentState?.parent?.componentMarkdown != null) 162 | DocMarkDown( 163 | markdown: currentState?.parent?.componentMarkdown ?? '', 164 | ), 165 | ...states 166 | .where((i) => i.parent == currentState?.parent) 167 | .map( 168 | (item) => DocPanel( 169 | component: item.builder( 170 | context, 171 | context.watch().storyProvider!, 172 | ), 173 | codeSample: item.codeSample, 174 | markdown: item.markdown, 175 | stateName: item.stateName, 176 | ), 177 | ) 178 | .toList(), 179 | ], 180 | ), 181 | ), 182 | ), 183 | ); 184 | } 185 | } 186 | 187 | class _InteractiveViewerCanvas extends StatefulWidget { 188 | final Widget? component; 189 | final TransformationController controller; 190 | _InteractiveViewerCanvas(this.component, this.controller); 191 | 192 | @override 193 | State<_InteractiveViewerCanvas> createState() => 194 | _InteractiveViewerCanvasState(); 195 | } 196 | 197 | class _InteractiveViewerCanvasState extends State<_InteractiveViewerCanvas> 198 | with TickerProviderStateMixin { 199 | Animation? _animationReset; 200 | late AnimationController _controllerReset; 201 | 202 | @override 203 | void initState() { 204 | super.initState(); 205 | _controllerReset = AnimationController( 206 | vsync: this, 207 | duration: const Duration(milliseconds: 400), 208 | ); 209 | } 210 | 211 | void _onAnimateReset() { 212 | widget.controller.value = _animationReset!.value; 213 | if (!_controllerReset.isAnimating) { 214 | _animationReset?.removeListener(_onAnimateReset); 215 | _animationReset = null; 216 | _controllerReset.reset(); 217 | } 218 | } 219 | 220 | void _animateResetInitialize() { 221 | _controllerReset.reset(); 222 | _animationReset = Matrix4Tween( 223 | begin: widget.controller.value, 224 | end: Matrix4.identity(), 225 | ).animate(_controllerReset); 226 | _animationReset?.addListener(_onAnimateReset); 227 | _controllerReset.forward(); 228 | } 229 | 230 | // Stop a running reset to home transform animation. 231 | void _animateResetStop() { 232 | _controllerReset.stop(); 233 | _animationReset?.removeListener(_onAnimateReset); 234 | _animationReset = null; 235 | _controllerReset.reset(); 236 | } 237 | 238 | void _onInteractionStart(ScaleStartDetails details) { 239 | // If the user tries to cause a transformation while the reset animation is 240 | // running, cancel the reset animation. 241 | if (_controllerReset.status == AnimationStatus.forward) { 242 | _animateResetStop(); 243 | } 244 | } 245 | 246 | @override 247 | Widget build(BuildContext context) { 248 | return Consumer( 249 | builder: (context, model, child) { 250 | model.addListener(() { 251 | if (!model.panEnabled) { 252 | _animateResetInitialize(); 253 | } 254 | }); 255 | 256 | return InteractiveViewer( 257 | boundaryMargin: EdgeInsets.all(double.infinity), 258 | child: widget.component ?? const SizedBox.shrink(), 259 | panEnabled: model.panEnabled, 260 | onInteractionStart: _onInteractionStart, 261 | transformationController: widget.controller, 262 | ); 263 | }, 264 | ); 265 | } 266 | } 267 | 268 | class Story extends StatelessWidget { 269 | const Story({Key? key}) : super(key: key); 270 | 271 | @override 272 | Widget build(BuildContext context) { 273 | final story = context.watch().currentStory; 274 | 275 | return story?.builder( 276 | context, context.watch().storyProvider!) ?? 277 | Container( 278 | color: context.colorScheme.onSecondary, 279 | child: Center( 280 | child: Column( 281 | mainAxisSize: MainAxisSize.min, 282 | children: [ 283 | Text('😉', style: context.textTheme.displayMedium), 284 | const SizedBox(height: 12), 285 | const Text('Select a Story!'), 286 | ], 287 | ), 288 | ), 289 | ); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /lib/src/editor/ui/editor_bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_feather_icons/flutter_feather_icons.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import '../../routing/story_provider.dart'; 6 | import '../../styled_widgets/styled_widgets.dart'; 7 | import '../../utils/extensions.dart'; 8 | 9 | enum FlutterBookTab { controls, actions, none } 10 | 11 | class CoreBottomBar extends StatefulWidget { 12 | const CoreBottomBar({Key? key}) : super(key: key); 13 | 14 | @override 15 | _CoreBottomBarState createState() => _CoreBottomBarState(); 16 | } 17 | 18 | class _CoreBottomBarState extends State { 19 | FlutterBookTab tab = FlutterBookTab.none; 20 | 21 | bool get isOpen => tab != FlutterBookTab.none; 22 | 23 | void changeControls(FlutterBookTab tab) { 24 | if (this.tab == tab) { 25 | setState(() => this.tab = FlutterBookTab.none); 26 | return; 27 | } 28 | setState(() => this.tab = tab); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return AnimatedSize( 34 | curve: Curves.linearToEaseOut, 35 | duration: const Duration(milliseconds: 300), 36 | child: Column( 37 | mainAxisSize: MainAxisSize.min, 38 | crossAxisAlignment: CrossAxisAlignment.stretch, 39 | children: [ 40 | Row( 41 | mainAxisSize: MainAxisSize.min, 42 | children: [ 43 | _TabButton( 44 | currentTab: tab, 45 | onPressed: () => changeControls(FlutterBookTab.controls), 46 | tab: FlutterBookTab.controls, 47 | text: 'Controls', 48 | ), 49 | if (isOpen) 50 | TextButton( 51 | onPressed: () => setState(() => tab = FlutterBookTab.none), 52 | style: TextButton.styleFrom( 53 | splashFactory: InkRipple.splashFactory, 54 | shape: RoundedRectangleBorder( 55 | borderRadius: BorderRadius.circular(90)), 56 | minimumSize: Size.zero, 57 | padding: const EdgeInsets.all(12), 58 | ), 59 | child: Icon( 60 | FeatherIcons.cornerRightDown, 61 | color: context.theme.hintColor, 62 | size: 16, 63 | ), 64 | ), 65 | ], 66 | ), 67 | Divider( 68 | height: 0, 69 | color: context.theme.dividerColor.withOpacity(0.5), 70 | ), 71 | if (isOpen) const _BottomBarContent(), 72 | ], 73 | ), 74 | ); 75 | } 76 | } 77 | 78 | class _BottomBarContent extends StatelessWidget { 79 | const _BottomBarContent({Key? key}) : super(key: key); 80 | 81 | Widget builderHeaders(BuildContext context) { 82 | final headerTextStyle = context.textTheme.titleSmall!.copyWith( 83 | fontWeight: FontWeight.w600, 84 | color: context.colorScheme.onSurface.withOpacity(0.5), 85 | ); 86 | 87 | return Row( 88 | children: [ 89 | Expanded( 90 | flex: 2, 91 | child: Text( 92 | 'Name', 93 | style: headerTextStyle, 94 | ), 95 | ), 96 | Expanded( 97 | flex: 2, 98 | child: Text( 99 | 'Description', 100 | style: headerTextStyle, 101 | ), 102 | ), 103 | Expanded( 104 | flex: 2, 105 | child: Text( 106 | 'Default', 107 | style: headerTextStyle, 108 | ), 109 | ), 110 | Expanded( 111 | flex: 2, 112 | child: Text( 113 | 'Controls', 114 | style: headerTextStyle, 115 | ), 116 | ), 117 | ], 118 | ); 119 | } 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | return Consumer( 124 | builder: (BuildContext context, StoryProvider value, Widget? child) { 125 | return Unfocuser( 126 | child: SizedBox( 127 | height: 300, 128 | width: double.infinity, 129 | child: ListView.separated( 130 | itemCount: value.all.length + 1, 131 | itemBuilder: (context, index) { 132 | late Widget current; 133 | if (index == 0) { 134 | current = builderHeaders(context); 135 | } else { 136 | current = value.all[index - 1].build(); 137 | } 138 | return Padding( 139 | padding: const EdgeInsets.symmetric( 140 | vertical: 12.0, horizontal: 24), 141 | child: current, 142 | ); 143 | }, 144 | separatorBuilder: (context, index) => Divider( 145 | height: 0, 146 | color: context.theme.dividerColor.withOpacity(0.1), 147 | ), 148 | ), 149 | ), 150 | ); 151 | }, 152 | ); 153 | } 154 | } 155 | 156 | class _TabButton extends StatelessWidget { 157 | const _TabButton({ 158 | Key? key, 159 | required this.text, 160 | required this.onPressed, 161 | required this.tab, 162 | required this.currentTab, 163 | }) : super(key: key); 164 | 165 | final String text; 166 | final VoidCallback onPressed; 167 | final FlutterBookTab tab; 168 | final FlutterBookTab currentTab; 169 | 170 | @override 171 | Widget build(BuildContext context) { 172 | final TextStyle selectedTabStyle = context.textTheme.titleMedium!.copyWith( 173 | fontWeight: FontWeight.bold, 174 | color: context.colorScheme.primary, 175 | ); 176 | 177 | final TextStyle tabStyle = context.textTheme.titleMedium!.copyWith( 178 | fontWeight: FontWeight.bold, 179 | color: context.theme.hintColor, 180 | ); 181 | 182 | return TextButton( 183 | onPressed: onPressed, 184 | child: AnimatedContainer( 185 | duration: const Duration(milliseconds: 200), 186 | decoration: BoxDecoration( 187 | border: Border( 188 | bottom: BorderSide( 189 | color: context.colorScheme.primary, 190 | style: currentTab == tab ? BorderStyle.solid : BorderStyle.none, 191 | width: 3, 192 | ), 193 | ), 194 | ), 195 | padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), 196 | child: Text( 197 | text, 198 | style: currentTab == tab ? selectedTabStyle : tabStyle, 199 | ), 200 | ), 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /lib/src/editor/ui/editor_tabs.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_feather_icons/flutter_feather_icons.dart'; 3 | import 'package:flutterbook/src/editor/editor.dart'; 4 | import 'package:flutterbook/src/editor/providers/device_preview_provider.dart'; 5 | import 'package:flutterbook/src/editor/providers/pan_provider.dart'; 6 | import 'package:flutterbook/src/editor/providers/tab_provider.dart'; 7 | import 'package:flutterbook/src/editor/ui/styled_icon_button.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | import '../../theme_provider.dart'; 11 | import '../../utils/extensions.dart'; 12 | 13 | enum FlutterBookTab { canvas, docs } 14 | 15 | class CoreContentTabs extends StatefulWidget { 16 | const CoreContentTabs({Key? key}) : super(key: key); 17 | 18 | @override 19 | _CoreContentTabsState createState() => _CoreContentTabsState(); 20 | } 21 | 22 | class _CoreContentTabsState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | final TextStyle selectedTabStyle = context.textTheme.titleMedium!.copyWith( 26 | fontWeight: FontWeight.bold, 27 | color: context.colorScheme.primary, 28 | ); 29 | 30 | final TextStyle tabStyle = context.textTheme.titleMedium!.copyWith( 31 | fontWeight: FontWeight.bold, 32 | color: context.theme.hintColor, 33 | ); 34 | 35 | return IntrinsicHeight( 36 | child: Row( 37 | children: [ 38 | TextButton( 39 | onPressed: () => Provider.of( 40 | context, 41 | listen: false, 42 | ).setTab(FlutterBookTab.canvas), 43 | child: AnimatedContainer( 44 | duration: const Duration(milliseconds: 200), 45 | decoration: BoxDecoration( 46 | border: Border( 47 | bottom: BorderSide( 48 | color: context.colorScheme.primary, 49 | style: context.watch().tab == 50 | FlutterBookTab.canvas 51 | ? BorderStyle.solid 52 | : BorderStyle.none, 53 | width: 3, 54 | ), 55 | ), 56 | ), 57 | padding: 58 | const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), 59 | child: Text( 60 | 'Canvas', 61 | style: context.read().tab == FlutterBookTab.canvas 62 | ? selectedTabStyle 63 | : tabStyle, 64 | ), 65 | ), 66 | ), 67 | TextButton( 68 | onPressed: () => Provider.of( 69 | context, 70 | listen: false, 71 | ).setTab(FlutterBookTab.docs), 72 | child: AnimatedContainer( 73 | duration: const Duration(milliseconds: 200), 74 | decoration: BoxDecoration( 75 | border: Border( 76 | bottom: BorderSide( 77 | color: context.colorScheme.primary, 78 | style: 79 | context.watch().tab == FlutterBookTab.docs 80 | ? BorderStyle.solid 81 | : BorderStyle.none, 82 | width: 3, 83 | ), 84 | ), 85 | ), 86 | padding: 87 | const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), 88 | child: Text( 89 | 'Docs', 90 | style: context.read().tab == FlutterBookTab.docs 91 | ? selectedTabStyle 92 | : tabStyle, 93 | ), 94 | ), 95 | ), 96 | const TabsVerticalDivider(), 97 | if (context.read().tab == FlutterBookTab.canvas) 98 | _CanvasTabs(), 99 | if (!context.watch().isUsingListOfThemes) 100 | StyledTextButton( 101 | icon: FeatherIcons.moon, 102 | onPressed: context.read().toggleDarkTheme, 103 | ), 104 | if (context.watch().isUsingListOfThemes) 105 | _MultiThemeDropdownButton(), 106 | ], 107 | ), 108 | ); 109 | } 110 | } 111 | 112 | class ThemeItem { 113 | final String name; 114 | final int index; 115 | ThemeItem({required this.name, required this.index}); 116 | } 117 | 118 | class _MultiThemeDropdownButton extends StatelessWidget { 119 | const _MultiThemeDropdownButton({Key? key}) : super(key: key); 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | List> themeItems = context 124 | .watch() 125 | .themeNames 126 | .asMap() 127 | .map((index, name) { 128 | return MapEntry( 129 | index, 130 | DropdownMenuItem( 131 | value: ThemeItem(name: name, index: index), 132 | child: Text(name), 133 | ), 134 | ); 135 | }) 136 | .values 137 | .toList(); 138 | 139 | return DropdownButton( 140 | items: themeItems, 141 | dropdownColor: Theme.of(context).scaffoldBackgroundColor, 142 | value: themeItems[context.watch().activeThemeIndex].value, 143 | onChanged: (ThemeItem? item) { 144 | if (item != null) { 145 | context.read().onChangeActiveThemeIndex(item.index); 146 | } 147 | }, 148 | ); 149 | } 150 | } 151 | 152 | class _CanvasTabs extends StatelessWidget { 153 | @override 154 | Widget build(BuildContext context) { 155 | const _spacer = const SizedBox(width: 8); 156 | return Row( 157 | children: [ 158 | StyledTextButton( 159 | icon: FeatherIcons.mousePointer, 160 | isActive: context.watch().panEnabled, 161 | onPressed: context.read().toggle, 162 | ), 163 | StyledTextButton( 164 | icon: FeatherIcons.zoomIn, 165 | onPressed: context.read().zoomIn, 166 | ), 167 | _spacer, 168 | StyledTextButton( 169 | icon: FeatherIcons.zoomOut, 170 | onPressed: context.read().zoomOut, 171 | ), 172 | _spacer, 173 | StyledTextButton( 174 | icon: FeatherIcons.refreshCcw, 175 | onPressed: context.read().resetZoom, 176 | ), 177 | StyledTextButton( 178 | icon: FeatherIcons.grid, 179 | isActive: context.watch().grid, 180 | onPressed: context.read().toggleGrid, 181 | ), 182 | const TabsVerticalDivider(), 183 | _spacer, 184 | StyledTextButton( 185 | icon: FeatherIcons.smartphone, 186 | isActive: context.watch().show, 187 | onPressed: context.read().togglePreview, 188 | ), 189 | _spacer, 190 | ], 191 | ); 192 | } 193 | } 194 | 195 | class TabsVerticalDivider extends StatelessWidget { 196 | const TabsVerticalDivider({Key? key}) : super(key: key); 197 | 198 | @override 199 | Widget build(BuildContext context) { 200 | return VerticalDivider( 201 | indent: 10, 202 | endIndent: 10, 203 | color: context.theme.dividerColor.withOpacity(0.5), 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/src/editor/ui/styled_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StyledTextButton extends StatelessWidget { 4 | final IconData icon; 5 | final bool isActive; 6 | final VoidCallback onPressed; 7 | 8 | StyledTextButton({ 9 | required this.icon, 10 | required this.onPressed, 11 | this.isActive = false, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: const EdgeInsets.all(12), 18 | child: InkWell( 19 | onTap: onPressed, 20 | borderRadius: BorderRadius.circular(90), 21 | child: Icon( 22 | icon, 23 | color: isActive 24 | ? Theme.of(context).colorScheme.primary 25 | : Theme.of(context).hintColor, 26 | size: 16, 27 | )), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/flutterbook.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutterbook/src/editor/providers/device_preview_provider.dart'; 3 | import 'package:flutterbook/src/editor/providers/pan_provider.dart'; 4 | import 'package:flutterbook/src/editor/providers/tab_provider.dart'; 5 | import 'package:flutterbook/src/utils/flutter_book_theme.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | import 'editor/editor.dart'; 9 | import 'navigation/navigation.dart'; 10 | import 'routing/router.dart'; 11 | import 'styled_widgets/styled_widgets.dart'; 12 | import 'theme_provider.dart'; 13 | import 'utils/utils.dart'; 14 | 15 | class FlutterBook extends StatefulWidget { 16 | /// Categories that can contain folders or components that can display states. 17 | /// States will have widgets which you may click on and display the widget in 18 | /// the editor. 19 | final List categories; 20 | 21 | /// The `ThemeData` that is defaulted when the project is opened. This should 22 | /// be considered as the light theme. 23 | final ThemeData? theme; 24 | 25 | /// The `ThemeData` used when the dark theme is enabled. 26 | final ThemeData? darkTheme; 27 | 28 | /// The branding/header of the project. This is displayed on the top left of the 29 | /// flutterbook. 30 | final Widget? header; 31 | 32 | /// The padding for the branding/header of the project. 33 | final EdgeInsetsGeometry headerPadding; 34 | 35 | /// Custom theme for the code sample shown on component state documentation. 36 | final CodeSampleThemeData? codeSampleTheme; 37 | 38 | /// This is used for projects that have more than two themes, if it is defined 39 | /// and not empty, then the `theme` and `darkTheme` will be ignored and a 40 | /// dropdown will appear in the editor tabs. 41 | final List? themes; 42 | 43 | final Locale? locale; 44 | final Iterable>? localizationsDelegates; 45 | final LocaleListResolutionCallback? localeListResolutionCallback; 46 | final LocaleResolutionCallback? localeResolutionCallback; 47 | final Iterable? supportedLocales; 48 | 49 | const FlutterBook({ 50 | Key? key, 51 | required this.categories, 52 | this.theme, 53 | this.darkTheme, 54 | this.codeSampleTheme, 55 | this.header, 56 | this.headerPadding = const EdgeInsets.fromLTRB(20, 16, 20, 8), 57 | this.themes, 58 | this.locale, 59 | this.localizationsDelegates, 60 | this.localeListResolutionCallback, 61 | this.localeResolutionCallback, 62 | this.supportedLocales, 63 | }) : super(key: key); 64 | 65 | @override 66 | _FlutterBookState createState() => _FlutterBookState(); 67 | } 68 | 69 | class _FlutterBookState extends State { 70 | GlobalKey navigator = GlobalKey(); 71 | Widget? selectedComponent; 72 | 73 | bool get useMultiTheme => widget.themes != null && widget.themes!.length > 1; 74 | List get themeNames => 75 | widget.themes?.map((theme) => theme.themeName).toList() ?? []; 76 | 77 | @override 78 | void initState() { 79 | if (widget.darkTheme != null) Styles.darkTheme = widget.darkTheme!; 80 | if (widget.theme != null) Styles.lightTheme = widget.theme!; 81 | 82 | super.initState(); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return MultiProvider( 88 | providers: [ 89 | Provider.value(value: widget.categories), 90 | ChangeNotifierProvider(create: (_) => CanvasDelegateProvider()), 91 | ChangeNotifierProvider( 92 | create: (_) => ThemeProvider( 93 | useListOfThemes: useMultiTheme, 94 | themeNames: themeNames, 95 | ), 96 | ), 97 | ChangeNotifierProvider(create: (_) => DevicePreviewProvider()), 98 | ChangeNotifierProvider(create: (_) => GridProvider()), 99 | ChangeNotifierProvider(create: (_) => TabProvider()), 100 | ChangeNotifierProvider(create: (_) => ZoomProvider()), 101 | ChangeNotifierProvider(create: (_) => PanProvider()), 102 | ], 103 | child: Consumer( 104 | builder: (BuildContext context, model, Widget? child) { 105 | ThemeData activeTheme = useMultiTheme 106 | ? widget.themes![model.activeThemeIndex].theme 107 | : widget.theme ?? Styles().theme; 108 | 109 | return MaterialApp( 110 | title: 'Flutterbook', 111 | debugShowCheckedModeBanner: false, 112 | theme: activeTheme, 113 | locale: widget.locale, 114 | localizationsDelegates: widget.localizationsDelegates, 115 | localeListResolutionCallback: widget.localeListResolutionCallback, 116 | localeResolutionCallback: widget.localeResolutionCallback, 117 | supportedLocales: 118 | widget.supportedLocales ?? const [Locale('en', 'US')], 119 | builder: (context, child) { 120 | return StyledScaffold( 121 | body: Row( 122 | children: [ 123 | NavigationPanel( 124 | header: widget.header, 125 | headerPadding: widget.headerPadding, 126 | categories: context.watch>().toList(), 127 | onComponentSelected: (child) { 128 | navigator.currentState!.pushReplacementNamed( 129 | '/stories/${child?.path ?? ''}'); 130 | context 131 | .read() 132 | .storyProvider! 133 | .updateStory(child); 134 | }, 135 | ), 136 | Expanded( 137 | child: Navigator( 138 | reportsRouteUpdateToEngine: true, 139 | key: navigator, 140 | initialRoute: '/', 141 | onGenerateRoute: (settings) => generateRoute( 142 | context, 143 | settings.name, 144 | settings: settings, 145 | ), 146 | ), 147 | ), 148 | ], 149 | ), 150 | ); 151 | }, 152 | ); 153 | }, 154 | ), 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/src/navigation/models/organizers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:recase/recase.dart'; 3 | 4 | import '../../routing/controls.dart'; 5 | 6 | abstract class Organizer { 7 | final List organizers; 8 | final OrganizerType type; 9 | final String name; 10 | Organizer? parent; 11 | 12 | /// Abstract class for organizer panel in the left. 13 | Organizer(this.name, this.type, this.organizers); 14 | } 15 | 16 | enum OrganizerType { category, component, folder } 17 | 18 | class Category extends Organizer { 19 | final String categoryName; 20 | 21 | Category({required this.categoryName, required List organizers}) 22 | : super(categoryName, OrganizerType.category, organizers) { 23 | for (final Organizer organizer in organizers) { 24 | organizer.parent = this; 25 | } 26 | } 27 | } 28 | 29 | class Folder extends Organizer { 30 | final String folderName; 31 | 32 | Folder({required this.folderName, required List organizers}) 33 | : super(folderName, OrganizerType.folder, organizers) { 34 | for (final Organizer organizer in organizers) { 35 | organizer.parent = this; 36 | } 37 | } 38 | } 39 | 40 | class Component extends Organizer { 41 | final String componentName; 42 | 43 | /// Markdown with documentation for the component. 44 | final String? componentMarkdown; 45 | final List states; 46 | 47 | Component({ 48 | required this.componentName, 49 | this.componentMarkdown, 50 | required this.states, 51 | }) : super(componentName, OrganizerType.component, const []) { 52 | for (final ComponentState state in states) { 53 | state.parent = this; 54 | } 55 | } 56 | } 57 | 58 | class ComponentState { 59 | Component? parent; 60 | final String? markdown; 61 | final String stateName; 62 | final String? codeSample; 63 | final Widget Function(BuildContext, ControlsInterface) builder; 64 | 65 | String get path { 66 | String path = ReCase(stateName).paramCase; 67 | Organizer? currentParent = parent; 68 | while (currentParent != null) { 69 | path = '${ReCase(currentParent.name).paramCase}${'/$path'}'; 70 | currentParent = currentParent.parent; 71 | } 72 | return path; 73 | } 74 | 75 | ComponentState({ 76 | required this.builder, 77 | required this.stateName, 78 | this.markdown, 79 | this.codeSample, 80 | }); 81 | 82 | factory ComponentState.center({ 83 | required Widget child, 84 | String? markdown, 85 | String? codeSample, 86 | required String stateName, 87 | }) => 88 | ComponentState( 89 | builder: (_, __) => Center(child: child), 90 | markdown: markdown, 91 | stateName: stateName, 92 | codeSample: codeSample, 93 | ); 94 | 95 | factory ComponentState.child({ 96 | required Widget child, 97 | String? markdown, 98 | String? codeSample, 99 | required String stateName, 100 | }) => 101 | ComponentState( 102 | builder: (_, __) => child, 103 | markdown: markdown, 104 | stateName: stateName, 105 | codeSample: codeSample, 106 | ); 107 | } 108 | 109 | class ListItem { 110 | final String title; 111 | final T value; 112 | ListItem({ 113 | required this.title, 114 | required this.value, 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/navigation/navigation.dart: -------------------------------------------------------------------------------- 1 | export 'models/organizers.dart'; 2 | export 'ui/navigation_panel.dart'; 3 | export 'ui/navigation_tiles.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/navigation/ui/navigation_panel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/foundation.dart' show kIsWeb; 5 | import 'package:flutter_feather_icons/flutter_feather_icons.dart'; 6 | 7 | import '../../styled_widgets/smooth_scroll.dart'; 8 | import '../../utils/utils.dart'; 9 | import '../navigation.dart'; 10 | 11 | class NavigationPanel extends StatefulWidget { 12 | const NavigationPanel({ 13 | Key? key, 14 | required this.categories, 15 | this.onComponentSelected, 16 | this.headerPadding, 17 | this.header, 18 | }) : super(key: key); 19 | 20 | final List categories; 21 | final void Function(ComponentState?)? onComponentSelected; 22 | final Widget? header; 23 | final EdgeInsetsGeometry? headerPadding; 24 | 25 | @override 26 | _NavigationPanelState createState() => _NavigationPanelState(); 27 | } 28 | 29 | class _NavigationPanelState extends State { 30 | final ScrollController controller = ScrollController(); 31 | ComponentState? selectedComponent; 32 | 33 | final TextEditingController search = TextEditingController(); 34 | String query = ''; 35 | 36 | void _onComponentSelected(ComponentState state) { 37 | ComponentState? current = state; 38 | if (current == selectedComponent) current = null; 39 | widget.onComponentSelected?.call(current); 40 | selectedComponent = current; 41 | setState(() {}); 42 | } 43 | 44 | Widget _buildCategories(BuildContext context, int i) { 45 | final Category item = widget.categories[i]; 46 | return Column( 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | mainAxisSize: MainAxisSize.min, 49 | children: [ 50 | Padding( 51 | padding: 52 | const EdgeInsets.only(top: 12.0, bottom: 4, left: 20, right: 20), 53 | child: Text( 54 | item.name, 55 | style: context.textTheme.displayMedium!.copyWith( 56 | fontWeight: FontWeight.w600, 57 | letterSpacing: 2, 58 | ), 59 | ), 60 | ), 61 | ..._buildOrganizers(item, 1, query) 62 | ], 63 | ); 64 | } 65 | 66 | List _buildOrganizers(Organizer category, int layer, String query) { 67 | final List organizers = []; 68 | final double layerPadding = layer * 6.0; 69 | for (final Organizer item in category.organizers) { 70 | final List current = []; 71 | switch (item.type) { 72 | case OrganizerType.folder: 73 | current.add( 74 | FolderTile( 75 | organizers: _buildOrganizers(item, layer + 3, query), 76 | padding: layerPadding, 77 | item: item, 78 | ), 79 | ); 80 | break; 81 | case OrganizerType.component: 82 | final lowerQuery = query.toLowerCase(); 83 | if (query.isEmpty || item.name.toLowerCase().contains(lowerQuery)) { 84 | current.add( 85 | ComponentTile( 86 | states: (item as Component) 87 | .states 88 | .map( 89 | (e) => ComponentStateTile( 90 | isSelected: selectedComponent == e, 91 | onSelected: _onComponentSelected, 92 | padding: layerPadding, 93 | item: e, 94 | ), 95 | ) 96 | .toList(), 97 | padding: layerPadding, 98 | item: item, 99 | ), 100 | ); 101 | } 102 | break; 103 | default: 104 | } 105 | organizers.add(Column(children: current)); 106 | } 107 | return organizers; 108 | } 109 | 110 | void _searchOrganizers(String query) { 111 | this.query = query; 112 | setState(() {}); 113 | } 114 | 115 | @override 116 | Widget build(BuildContext context) { 117 | return Container( 118 | constraints: const BoxConstraints(minWidth: 50, maxWidth: 250), 119 | child: Column( 120 | children: [ 121 | widget.header ?? const _NavigationHeader(), 122 | SizedBox( 123 | height: 50, 124 | child: Padding( 125 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), 126 | child: Overlay( 127 | initialEntries: [ 128 | OverlayEntry( 129 | builder: (context) => CupertinoSearchTextField( 130 | itemSize: 16, 131 | controller: search, 132 | onChanged: _searchOrganizers, 133 | onSuffixTap: () { 134 | search.text = ''; 135 | _searchOrganizers(''); 136 | }, 137 | ), 138 | ), 139 | ], 140 | ), 141 | ), 142 | ), 143 | Expanded( 144 | child: Builder( 145 | builder: (context) { 146 | if (kIsWeb) { 147 | // If web, we just disable smooth scrolling. 148 | return ListView.separated( 149 | controller: controller, 150 | physics: const AlwaysScrollableScrollPhysics(), 151 | itemCount: widget.categories.length, 152 | itemBuilder: _buildCategories, 153 | padding: const EdgeInsets.only(bottom: 8), 154 | separatorBuilder: (context, index) => 155 | const SizedBox(height: 8), 156 | ); 157 | } else { 158 | // If windows or macos we can allow smooth scrolling. 159 | if (Platform.isMacOS || Platform.isWindows) { 160 | return SmoothScroll( 161 | controller: controller, 162 | curve: Curves.easeOutExpo, 163 | scrollSpeed: 50, 164 | child: ListView.separated( 165 | controller: controller, 166 | physics: const NeverScrollableScrollPhysics(), 167 | itemCount: widget.categories.length, 168 | itemBuilder: _buildCategories, 169 | padding: const EdgeInsets.only(bottom: 8), 170 | separatorBuilder: (context, index) => 171 | const SizedBox(height: 8), 172 | ), 173 | ); 174 | } else { 175 | // If it's mobile then we're not using smooth scrolling. 176 | return ListView.separated( 177 | controller: controller, 178 | physics: const AlwaysScrollableScrollPhysics(), 179 | itemCount: widget.categories.length, 180 | itemBuilder: _buildCategories, 181 | padding: const EdgeInsets.only(bottom: 8), 182 | separatorBuilder: (context, index) => 183 | const SizedBox(height: 8), 184 | ); 185 | } 186 | } 187 | }, 188 | ), 189 | ), 190 | ], 191 | ), 192 | ); 193 | } 194 | } 195 | 196 | class _NavigationHeader extends StatelessWidget { 197 | const _NavigationHeader({Key? key}) : super(key: key); 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | return Padding( 202 | padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), 203 | child: Column( 204 | mainAxisSize: MainAxisSize.min, 205 | children: [ 206 | Container( 207 | alignment: Alignment.center, 208 | height: 50, 209 | child: Row( 210 | mainAxisAlignment: MainAxisAlignment.center, 211 | children: [ 212 | const Icon(FeatherIcons.bookOpen, color: Color(0xFFD689EF)), 213 | const SizedBox(width: 8), 214 | Text( 215 | 'Flutterbook', 216 | style: context.textTheme.headlineSmall! 217 | .copyWith(fontWeight: FontWeight.bold), 218 | ), 219 | ], 220 | ), 221 | ), 222 | Text( 223 | 'by MOONSDONTBURN', 224 | style: context.textTheme.bodySmall, 225 | ), 226 | ], 227 | ), 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/src/navigation/ui/navigation_tiles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_feather_icons/flutter_feather_icons.dart'; 4 | 5 | import '../../styled_widgets/highlighter.dart'; 6 | import '../../utils/utils.dart'; 7 | import '../models/organizers.dart'; 8 | 9 | //ignore_for_file: must_call_super 10 | 11 | class ComponentStateTile extends StatefulWidget { 12 | const ComponentStateTile({ 13 | Key? key, 14 | required this.item, 15 | required this.padding, 16 | required this.onSelected, 17 | this.isSelected = false, 18 | }) : super(key: key); 19 | 20 | final void Function(ComponentState) onSelected; 21 | final ComponentState item; 22 | final double padding; 23 | final bool isSelected; 24 | 25 | @override 26 | _ComponentStateTileState createState() => _ComponentStateTileState(); 27 | } 28 | 29 | class _ComponentStateTileState extends State 30 | with AutomaticKeepAliveClientMixin { 31 | bool hover = false; 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | final Color hoverColor = widget.isSelected || hover 36 | ? context.colorScheme.onPrimary 37 | : context.colorScheme.onSurface; 38 | 39 | return Highlighter( 40 | onEnter: (_) => setState(() => hover = true), 41 | onExit: (_) => setState(() => hover = false), 42 | overrideHover: widget.isSelected, 43 | onPressed: () => widget.onSelected(widget.item), 44 | child: Container( 45 | padding: EdgeInsets.fromLTRB(widget.padding, 4, 0, 4), 46 | child: Row( 47 | children: [ 48 | const SizedBox(width: 32), 49 | Icon( 50 | FeatherIcons.bookmark, 51 | size: 14, 52 | color: widget.isSelected || hover 53 | ? context.colorScheme.onPrimary 54 | : context.colorScheme.secondary, 55 | ), 56 | const SizedBox(width: 8), 57 | Text( 58 | widget.item.stateName, 59 | style: context.textTheme.bodyLarge!.copyWith(color: hoverColor), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ); 65 | } 66 | 67 | @override 68 | bool get wantKeepAlive => true; 69 | } 70 | 71 | class ComponentTile extends StatefulWidget { 72 | const ComponentTile({ 73 | Key? key, 74 | required this.states, 75 | required this.padding, 76 | required this.item, 77 | }) : super(key: key); 78 | 79 | final List states; 80 | final Organizer item; 81 | final double padding; 82 | 83 | @override 84 | _ComponentTileState createState() => _ComponentTileState(); 85 | } 86 | 87 | class _ComponentTileState extends State 88 | with AutomaticKeepAliveClientMixin { 89 | bool expanded = false; 90 | bool hover = false; 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | final Color hoverColor = 95 | hover ? context.colorScheme.onPrimary : context.colorScheme.onSurface; 96 | 97 | return Column( 98 | mainAxisSize: MainAxisSize.min, 99 | children: [ 100 | Highlighter( 101 | onEnter: (_) => setState(() => hover = true), 102 | onExit: (_) => setState(() => hover = false), 103 | onPressed: () => setState(() => expanded = !expanded), 104 | cursor: 105 | (widget.states.isNotEmpty) ? null : SystemMouseCursors.forbidden, 106 | child: Container( 107 | padding: EdgeInsets.fromLTRB(widget.padding, 4, 0, 4), 108 | child: Row( 109 | children: [ 110 | if (widget.states.isNotEmpty) 111 | Icon(FeatherIcons.chevronDown, size: 12, color: hoverColor) 112 | else 113 | const SizedBox(width: 12), 114 | const SizedBox(width: 4), 115 | Icon( 116 | FeatherIcons.book, 117 | size: 14, 118 | color: hover 119 | ? context.colorScheme.onPrimary 120 | : context.colorScheme.secondaryContainer, 121 | ), 122 | const SizedBox(width: 8), 123 | Text( 124 | widget.item.name, 125 | style: 126 | context.textTheme.bodyLarge!.copyWith(color: hoverColor), 127 | ), 128 | ], 129 | ), 130 | ), 131 | ), 132 | AnimatedSwitcher( 133 | duration: const Duration(milliseconds: 100), 134 | child: expanded ? Column(children: widget.states) : Container(), 135 | ), 136 | ], 137 | ); 138 | } 139 | 140 | @override 141 | bool get wantKeepAlive => true; 142 | } 143 | 144 | class FolderTile extends StatefulWidget { 145 | const FolderTile({ 146 | Key? key, 147 | required this.organizers, 148 | required this.padding, 149 | required this.item, 150 | }) : super(key: key); 151 | 152 | final List organizers; 153 | final Organizer item; 154 | final double padding; 155 | 156 | @override 157 | _FolderTileState createState() => _FolderTileState(); 158 | } 159 | 160 | class _FolderTileState extends State 161 | with AutomaticKeepAliveClientMixin { 162 | bool expanded = false; 163 | bool hover = false; 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | final Color hoverColor = 168 | hover ? context.colorScheme.onPrimary : context.colorScheme.onSurface; 169 | 170 | return Column( 171 | mainAxisSize: MainAxisSize.min, 172 | children: [ 173 | Highlighter( 174 | onEnter: (_) => setState(() => hover = true), 175 | onExit: (_) => setState(() => hover = false), 176 | onPressed: () => setState(() => expanded = !expanded), 177 | cursor: (widget.organizers.isNotEmpty) 178 | ? null 179 | : SystemMouseCursors.forbidden, 180 | child: Container( 181 | padding: EdgeInsets.fromLTRB(widget.padding, 4, 0, 4), 182 | child: Row( 183 | children: [ 184 | if (widget.organizers.isNotEmpty) 185 | Icon(FeatherIcons.chevronDown, size: 12, color: hoverColor) 186 | else 187 | const SizedBox(width: 12), 188 | const SizedBox(width: 4), 189 | Icon( 190 | FeatherIcons.archive, 191 | size: 14, 192 | color: hover 193 | ? context.colorScheme.onPrimary 194 | : context.colorScheme.primaryContainer, 195 | ), 196 | const SizedBox(width: 8), 197 | Text( 198 | widget.item.name, 199 | style: 200 | context.textTheme.bodyLarge!.copyWith(color: hoverColor), 201 | ), 202 | ], 203 | ), 204 | ), 205 | ), 206 | AnimatedSwitcher( 207 | duration: const Duration(milliseconds: 100), 208 | child: expanded ? Column(children: widget.organizers) : Container(), 209 | ), 210 | ], 211 | ); 212 | } 213 | 214 | @override 215 | bool get wantKeepAlive => true; 216 | } 217 | -------------------------------------------------------------------------------- /lib/src/routing/controls.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | import '../../flutterbook.dart'; 7 | import '../editor/editor.dart'; 8 | import '../utils/utils.dart'; 9 | 10 | abstract class Control { 11 | Control(this.label, this.value, this.initial, this.description); 12 | 13 | final String description; 14 | final String label; 15 | final T initial; 16 | T value; 17 | 18 | Widget build(); 19 | } 20 | 21 | abstract class ControlsInterface { 22 | bool boolean({ 23 | required String label, 24 | required bool initial, 25 | String description, 26 | }); 27 | 28 | String text({ 29 | required String label, 30 | required String initial, 31 | String description, 32 | }); 33 | 34 | double number({ 35 | required String label, 36 | required double initial, 37 | String description, 38 | double max = 1, 39 | double min = 0, 40 | }); 41 | 42 | T list({ 43 | required String label, 44 | required T initial, 45 | required List> list, 46 | required T value, 47 | String description, 48 | }); 49 | } 50 | 51 | class BoolControl extends Control { 52 | BoolControl(String label, bool value, bool initial, String description) 53 | : super(label, value, initial, description); 54 | 55 | @override 56 | Widget build() => BoolControlWidget(label, value, initial, description); 57 | } 58 | 59 | class BoolControlWidget extends StatefulWidget { 60 | const BoolControlWidget( 61 | this.label, this.value, this.initial, this.description, 62 | {Key? key}) 63 | : super(key: key); 64 | 65 | final String label; 66 | final bool value; 67 | final bool initial; 68 | final String description; 69 | 70 | @override 71 | _BoolControlWidgetState createState() => _BoolControlWidgetState(); 72 | } 73 | 74 | class _BoolControlWidgetState extends State { 75 | late bool value; 76 | 77 | @override 78 | void initState() { 79 | value = widget.initial; 80 | super.initState(); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return _BaseControlLayout( 86 | label: widget.label, 87 | desc: widget.description, 88 | def: widget.initial.toString(), 89 | control: Row( 90 | children: [ 91 | Expanded(child: Text(value.toString())), 92 | const SizedBox(width: 4), 93 | Expanded( 94 | flex: 6, 95 | child: Align( 96 | alignment: Alignment.centerLeft, 97 | child: CupertinoSwitch( 98 | value: widget.value, 99 | activeColor: context.colorScheme.primary, 100 | onChanged: (val) { 101 | context 102 | .read() 103 | .storyProvider! 104 | .update(widget.label, val); 105 | value = val; 106 | setState(() {}); 107 | }, 108 | ), 109 | ), 110 | ), 111 | ], 112 | ), 113 | ); 114 | } 115 | } 116 | 117 | class StringControl extends Control { 118 | StringControl(String label, String value, String initial, String description) 119 | : super(label, value, initial, description); 120 | 121 | @override 122 | Widget build() => StringControlWidget(label, value, initial, description); 123 | } 124 | 125 | class StringControlWidget extends StatelessWidget { 126 | const StringControlWidget( 127 | this.label, this.value, this.initial, this.description, 128 | {Key? key}) 129 | : super(key: key); 130 | 131 | final String label; 132 | final String value; 133 | final String initial; 134 | final String description; 135 | 136 | @override 137 | Widget build(BuildContext context) { 138 | return _BaseControlLayout( 139 | label: label, 140 | desc: description, 141 | def: initial.toString(), 142 | control: TextFormField( 143 | initialValue: initial, 144 | cursorWidth: 1, 145 | onChanged: (str) => context 146 | .read() 147 | .storyProvider! 148 | .update(label, str), 149 | scrollPadding: EdgeInsets.zero, 150 | decoration: InputDecoration( 151 | hintText: initial, 152 | contentPadding: const EdgeInsets.fromLTRB(0, 2, 0, 8), 153 | ), 154 | ), 155 | ); 156 | } 157 | } 158 | 159 | class NumberControl extends Control { 160 | NumberControl(String label, double value, double initial, String description, 161 | this.min, this.max) 162 | : super(label, value, initial, description); 163 | 164 | final double min; 165 | final double max; 166 | 167 | @override 168 | Widget build() => 169 | NumberControlWidget(label, value, initial, description, min, max); 170 | } 171 | 172 | class NumberControlWidget extends StatefulWidget { 173 | const NumberControlWidget(this.label, this.value, this.initial, 174 | this.description, this.min, this.max, 175 | {Key? key}) 176 | : super(key: key); 177 | 178 | final String label; 179 | final double value; 180 | final double initial; 181 | final String description; 182 | final double min; 183 | final double max; 184 | 185 | @override 186 | _NumberControlWidgetState createState() => _NumberControlWidgetState(); 187 | } 188 | 189 | class _NumberControlWidgetState extends State { 190 | late double value; 191 | 192 | @override 193 | void initState() { 194 | value = widget.initial; 195 | super.initState(); 196 | } 197 | 198 | @override 199 | Widget build(BuildContext context) { 200 | return _BaseControlLayout( 201 | label: widget.label, 202 | desc: widget.description, 203 | def: widget.initial.toString(), 204 | control: Row( 205 | children: [ 206 | Expanded(child: Text(value.toStringAsFixed(0))), 207 | const SizedBox(width: 4), 208 | Expanded( 209 | flex: 6, 210 | child: CupertinoSlider( 211 | value: value, 212 | min: widget.min, 213 | max: widget.max, 214 | onChanged: (val) { 215 | context 216 | .read() 217 | .storyProvider! 218 | .update(widget.label, val); 219 | value = val; 220 | setState(() {}); 221 | }, 222 | ), 223 | ), 224 | ], 225 | ), 226 | ); 227 | } 228 | } 229 | 230 | class _BaseControlLayout extends StatelessWidget { 231 | const _BaseControlLayout({ 232 | Key? key, 233 | required this.label, 234 | required this.desc, 235 | required this.def, 236 | required this.control, 237 | }) : super(key: key); 238 | 239 | final String label; 240 | final String desc; 241 | final String def; 242 | final Widget control; 243 | 244 | @override 245 | Widget build(BuildContext context) { 246 | return Row( 247 | crossAxisAlignment: CrossAxisAlignment.start, 248 | children: [ 249 | Expanded( 250 | flex: 2, 251 | child: Text( 252 | label, 253 | style: context.textTheme.bodyLarge! 254 | .copyWith(fontWeight: FontWeight.bold), 255 | ), 256 | ), 257 | Expanded( 258 | flex: 2, 259 | child: Text(desc), 260 | ), 261 | Expanded( 262 | flex: 2, 263 | child: Text(def), 264 | ), 265 | Expanded( 266 | flex: 2, 267 | child: control, 268 | ), 269 | ], 270 | ); 271 | } 272 | } 273 | 274 | class ListControl extends Control { 275 | final List> list; 276 | ListControl(String label, T value, T initial, String description, this.list) 277 | : super(label, value, initial, description); 278 | 279 | @override 280 | Widget build() => 281 | ListControlWidget(label, value, initial, description, list); 282 | } 283 | 284 | class ListControlWidget extends StatefulWidget { 285 | const ListControlWidget( 286 | this.label, this.value, this.initial, this.description, this.list, 287 | {Key? key}) 288 | : super(key: key); 289 | 290 | final String label; 291 | final T value; 292 | final T initial; 293 | final String description; 294 | final List> list; 295 | 296 | @override 297 | State> createState() => _ListControlWidgetState(); 298 | } 299 | 300 | class _ListControlWidgetState extends State> { 301 | late ListItem? value; 302 | late List> listValues; 303 | 304 | @override 305 | void initState() { 306 | listValues = widget.list; 307 | if (widget.list.any((element) => element.title == widget.initial)) { 308 | value = widget.list 309 | .firstWhere((element) => element.title == widget.initial); 310 | } else { 311 | value = widget.list.first; 312 | } 313 | 314 | super.initState(); 315 | } 316 | 317 | @override 318 | Widget build(BuildContext context) { 319 | List>> items = listValues 320 | .asMap() 321 | .map((index, name) { 322 | return MapEntry( 323 | index, 324 | DropdownMenuItem>( 325 | value: name, 326 | child: Text(name.title.toString()), 327 | ), 328 | ); 329 | }) 330 | .values 331 | .toList(); 332 | 333 | return _BaseControlLayout( 334 | label: widget.label, 335 | desc: widget.description, 336 | def: widget.initial.toString(), 337 | control: DropdownButton>( 338 | items: items, 339 | dropdownColor: Theme.of(context).scaffoldBackgroundColor, 340 | value: value, 341 | onChanged: (ListItem? item) { 342 | if (item != null) { 343 | setState(() { 344 | context 345 | .read() 346 | .storyProvider! 347 | .update(widget.label, item.value); 348 | value = item; 349 | }); 350 | } 351 | }, 352 | ), 353 | ); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /lib/src/routing/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import '../editor/editor.dart'; 5 | import '../navigation/models/organizers.dart'; 6 | import '../navigation/navigation.dart'; 7 | import 'story_provider.dart'; 8 | 9 | ModalRoute generateRoute( 10 | BuildContext context, 11 | String? name, { 12 | RouteSettings? settings, 13 | }) { 14 | ChangeNotifierProvider builder(context) { 15 | return ChangeNotifierProvider( 16 | create: (context) { 17 | final provider = StoryProvider.fromPath( 18 | name, recursiveRetrievalOfStates(context.read>())); 19 | context.read().storyProvider = provider; 20 | return provider; 21 | }, 22 | child: Builder( 23 | builder: (context) => const Editor(component: Story()), 24 | ), 25 | ); 26 | } 27 | 28 | return StoryRoute(settings: settings, builder: builder); 29 | } 30 | 31 | class StoryRoute extends PopupRoute { 32 | StoryRoute({ 33 | required this.builder, 34 | RouteSettings? settings, 35 | }) : super(settings: settings); 36 | 37 | final WidgetBuilder builder; 38 | 39 | @override 40 | Color? get barrierColor => null; 41 | 42 | @override 43 | bool get barrierDismissible => false; 44 | 45 | @override 46 | String? get barrierLabel => null; 47 | 48 | @override 49 | Widget buildPage( 50 | BuildContext context, 51 | Animation animation, 52 | Animation secondaryAnimation, 53 | ) => 54 | builder(context); 55 | 56 | @override 57 | Duration get transitionDuration => const Duration(); 58 | } 59 | 60 | List recursiveRetrievalOfStates(List organizers) { 61 | final List states = []; 62 | for (final Organizer current in organizers) { 63 | if (current is Component) { 64 | states.addAll(current.states); 65 | } else { 66 | states.addAll(recursiveRetrievalOfStates(current.organizers)); 67 | } 68 | } 69 | return states; 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/routing/story_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | import '../navigation/models/organizers.dart'; 4 | import 'controls.dart'; 5 | 6 | class StoryProvider extends ChangeNotifier implements ControlsInterface { 7 | StoryProvider({ 8 | ComponentState? currentStory, 9 | }) : _currentStory = currentStory; 10 | 11 | factory StoryProvider.fromPath(String? path, List stories) { 12 | final storyPath = path?.replaceFirst('/stories/', '') ?? ''; 13 | ComponentState? story; 14 | for (final element in stories) { 15 | if (element.path == storyPath) story = element; 16 | } 17 | return StoryProvider(currentStory: story); 18 | } 19 | 20 | ComponentState? _currentStory; 21 | 22 | ComponentState? get currentStory => _currentStory; 23 | 24 | void updateStory(ComponentState? story) { 25 | _currentStory = story; 26 | notifyListeners(); 27 | } 28 | 29 | final Map _controls = {}; 30 | static const String kDefaultNoDescMessage = 'No description.'; 31 | bool _disposed = false; 32 | 33 | @override 34 | bool boolean( 35 | {required String label, 36 | bool initial = false, 37 | String description = kDefaultNoDescMessage}) => 38 | addControl(BoolControl(label, initial, initial, description)); 39 | 40 | @override 41 | String text( 42 | {required String label, 43 | String initial = '', 44 | String description = kDefaultNoDescMessage}) => 45 | addControl(StringControl(label, initial, initial, description)); 46 | 47 | T addControl(Control value) => 48 | (_controls.putIfAbsent(value.label, () => value) as Control).value; 49 | 50 | void update(String label, T value) { 51 | _controls[label]!.value = value; 52 | notifyListeners(); 53 | } 54 | 55 | T get(String label) => _controls[label]!.value as T; 56 | 57 | List get all => _controls.values.toList(); 58 | 59 | @override 60 | double number({ 61 | required String label, 62 | required double initial, 63 | double max = 1, 64 | double min = 0, 65 | String description = kDefaultNoDescMessage, 66 | }) => 67 | addControl(NumberControl(label, initial, initial, description, min, max)); 68 | 69 | @override 70 | void dispose() { 71 | _disposed = true; 72 | super.dispose(); 73 | } 74 | 75 | @override 76 | void notifyListeners() { 77 | if (!_disposed) { 78 | super.notifyListeners(); 79 | } 80 | } 81 | 82 | @override 83 | T list({ 84 | required String label, 85 | required T initial, 86 | required List> list, 87 | required T value, 88 | String description = kDefaultNoDescMessage, 89 | }) { 90 | return addControl(ListControl(label, value, initial, description, list)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/styled_widgets/animations.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/src/styled_widgets/highlighter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import '../utils/utils.dart'; 4 | 5 | class Highlighter extends StatefulWidget { 6 | const Highlighter({ 7 | Key? key, 8 | required this.child, 9 | this.color, 10 | this.onPressed, 11 | this.onEnter, 12 | this.onExit, 13 | this.cursor, 14 | this.overrideHover = false, 15 | }) : super(key: key); 16 | 17 | final Widget child; 18 | final Color? color; 19 | final VoidCallback? onPressed; 20 | final PointerEnterEventListener? onEnter; 21 | final PointerExitEventListener? onExit; 22 | final MouseCursor? cursor; 23 | final bool overrideHover; 24 | 25 | @override 26 | _HighlighterState createState() => _HighlighterState(); 27 | } 28 | 29 | class _HighlighterState extends State { 30 | bool hovered = false; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return GestureDetector( 35 | onTap: widget.onPressed, 36 | child: MouseRegion( 37 | onEnter: (e) { 38 | widget.onEnter?.call(e); 39 | setState(() => hovered = true); 40 | }, 41 | onExit: (e) { 42 | widget.onExit?.call(e); 43 | setState(() => hovered = false); 44 | }, 45 | cursor: widget.cursor ?? SystemMouseCursors.click, 46 | child: AnimatedContainer( 47 | decoration: BoxDecoration( 48 | color: widget.overrideHover || hovered 49 | ? widget.color ?? context.colorScheme.primary 50 | : null, 51 | // borderRadius: mainBorderRadius, 52 | ), 53 | duration: const Duration(milliseconds: 100), 54 | child: widget.child, 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/styled_widgets/smooth_scroll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const int kDefaultScrollDuration = 250; 5 | const int kDefaultScrollSpeed = 130; 6 | 7 | class SmoothScroll extends StatefulWidget { 8 | final ScrollController controller; 9 | 10 | final Widget child; 11 | 12 | final int scrollSpeed; 13 | 14 | final int scrollDuration; 15 | 16 | final Curve curve; 17 | 18 | const SmoothScroll({ 19 | Key? key, 20 | required this.controller, 21 | required this.child, 22 | this.scrollSpeed = kDefaultScrollSpeed, 23 | this.scrollDuration = kDefaultScrollDuration, 24 | this.curve = Curves.linear, 25 | }) : super(key: key); 26 | 27 | @override 28 | _SmoothScrollState createState() => _SmoothScrollState(); 29 | } 30 | 31 | class _SmoothScrollState extends State { 32 | double _scroll = 0; 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | widget.controller.addListener(() { 37 | // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member 38 | if (widget.controller.position.activity is IdleScrollActivity) { 39 | _scroll = widget.controller.position.extentBefore; 40 | } 41 | }); 42 | 43 | return Listener( 44 | onPointerSignal: (pointerSignal) { 45 | int millis = widget.scrollDuration; 46 | if (pointerSignal is PointerScrollEvent) { 47 | if (pointerSignal.scrollDelta.dy > 0) { 48 | _scroll += widget.scrollSpeed; 49 | } else { 50 | _scroll -= widget.scrollSpeed; 51 | } 52 | if (_scroll > widget.controller.position.maxScrollExtent) { 53 | _scroll = widget.controller.position.maxScrollExtent; 54 | millis = widget.scrollDuration ~/ 2; 55 | } 56 | if (_scroll < 0) { 57 | _scroll = 0; 58 | millis = widget.scrollDuration ~/ 2; 59 | } 60 | 61 | widget.controller.animateTo( 62 | _scroll, 63 | duration: Duration(milliseconds: millis), 64 | curve: widget.curve, 65 | ); 66 | } 67 | }, 68 | child: widget.child, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/styled_widgets/styled_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StyledScaffold extends StatelessWidget { 4 | final Widget? body; 5 | 6 | const StyledScaffold({Key? key, this.body}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return GestureDetector( 11 | onTap: WidgetsBinding.instance.focusManager.primaryFocus?.unfocus, 12 | child: Scaffold( 13 | body: body, 14 | ), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/styled_widgets/styled_widgets.dart: -------------------------------------------------------------------------------- 1 | export 'animations.dart'; 2 | export 'highlighter.dart'; 3 | export 'smooth_scroll.dart'; 4 | export 'styled_scaffold.dart'; 5 | export 'unfocuser.dart'; -------------------------------------------------------------------------------- /lib/src/styled_widgets/unfocuser.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Unfocuser extends StatelessWidget { 4 | const Unfocuser({Key? key, required this.child}) : super(key: key); 5 | 6 | final Widget child; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return GestureDetector( 11 | behavior: HitTestBehavior.translucent, 12 | onTap: WidgetsBinding.instance.focusManager.primaryFocus?.unfocus, 13 | child: child, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'utils/utils.dart'; 3 | 4 | class ThemeProvider with ChangeNotifier { 5 | bool _darkTheme = false; 6 | 7 | bool _isUsingListOfThemes = false; 8 | 9 | int _activeThemeIndex = 0; 10 | 11 | int get activeThemeIndex => _activeThemeIndex; 12 | 13 | bool get darkTheme => _darkTheme; 14 | 15 | bool get isUsingListOfThemes => _isUsingListOfThemes; 16 | 17 | List themeNames = []; 18 | 19 | set darkTheme(bool value) { 20 | _darkTheme = value; 21 | Styles.isDark = value; 22 | notifyListeners(); 23 | } 24 | 25 | ThemeProvider({useListOfThemes = false, this.themeNames = const []}) { 26 | _isUsingListOfThemes = useListOfThemes; 27 | } 28 | 29 | void toggleDarkTheme([bool? value]) => darkTheme = value ?? !_darkTheme; 30 | 31 | void onChangeActiveThemeIndex(int index) { 32 | _activeThemeIndex = index; 33 | notifyListeners(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/utils/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ContextExtension on BuildContext { 4 | ColorScheme get colorScheme => Theme.of(this).colorScheme; 5 | TextTheme get textTheme => Theme.of(this).textTheme; 6 | MediaQueryData get mediaQuery => MediaQuery.of(this); 7 | ThemeData get theme => Theme.of(this); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/utils/flutter_book_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FlutterBookTheme { 4 | final ThemeData theme; 5 | final String themeName; 6 | 7 | FlutterBookTheme({required this.theme, required this.themeName}); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/utils/radii.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const double mainRadiusValue = 4; 4 | const Radius mainRadius = Radius.circular(mainRadiusValue); 5 | const BorderRadius mainBorderRadius = BorderRadius.all(mainRadius); 6 | 7 | const double canvasRadiusValue = 6; 8 | const Radius canvasRadius = Radius.circular(canvasRadiusValue); 9 | const BorderRadius canvasBorderRadius = BorderRadius.all(canvasRadius); 10 | -------------------------------------------------------------------------------- /lib/src/utils/ripple.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BookRippleFactory extends InteractiveInkFeatureFactory { 4 | const BookRippleFactory(); 5 | 6 | @override 7 | InteractiveInkFeature create({ 8 | required MaterialInkController controller, 9 | required RenderBox referenceBox, 10 | required Offset position, 11 | required Color color, 12 | required TextDirection textDirection, 13 | bool containedInkWell = false, 14 | RectCallback? rectCallback, 15 | BorderRadius? borderRadius, 16 | ShapeBorder? customBorder, 17 | double? radius, 18 | VoidCallback? onRemoved, 19 | }) { 20 | return InkRipple( 21 | controller: controller, 22 | referenceBox: referenceBox, 23 | position: position, 24 | color: color, 25 | onRemoved: onRemoved, 26 | ); 27 | } 28 | } 29 | 30 | const Duration _kUnconfirmedRippleDuration = Duration(seconds: 1); 31 | const Duration _kFadeInDuration = Duration(milliseconds: 75); 32 | const Duration _kRadiusDuration = Duration(milliseconds: 225); 33 | const Duration _kFadeOutDuration = Duration(milliseconds: 375); 34 | const Duration _kCancelDuration = Duration(milliseconds: 75); 35 | 36 | class InkRipple extends InteractiveInkFeature { 37 | /// Begin a ripple, centered at [position] relative to [referenceBox]. 38 | /// 39 | /// The [controller] argument is typically obtained via 40 | /// `Material.of(context)`. 41 | /// 42 | /// If [containedInkWell] is true, then the ripple will be sized to fit 43 | /// the well rectangle, then clipped to it when drawn. The well 44 | /// rectangle is the box returned by [rectCallback], if provided, or 45 | /// otherwise is the bounds of the [referenceBox]. 46 | /// 47 | /// If [containedInkWell] is false, then [rectCallback] should be null. 48 | /// The ink ripple is clipped only to the edges of the [Material]. 49 | /// This is the default. 50 | /// 51 | /// When the ripple is removed, [onRemoved] will be called. 52 | InkRipple({ 53 | required MaterialInkController controller, 54 | required RenderBox referenceBox, 55 | required Offset position, 56 | required Color color, 57 | VoidCallback? onRemoved, 58 | }) : _position = position, 59 | super( 60 | controller: controller, 61 | referenceBox: referenceBox, 62 | color: color, 63 | onRemoved: onRemoved) { 64 | // Immediately begin fading-in the initial splash. 65 | _fadeInController = 66 | AnimationController(duration: _kFadeInDuration, vsync: controller.vsync) 67 | ..addListener(controller.markNeedsPaint) 68 | ..forward(); 69 | _fadeIn = _fadeInController.drive(IntTween( 70 | begin: 0, 71 | end: color.alpha, 72 | )); 73 | 74 | // Controls the splash radius and its center. Starts upon confirm. 75 | _radiusController = AnimationController( 76 | duration: _kUnconfirmedRippleDuration, vsync: controller.vsync) 77 | ..addListener(controller.markNeedsPaint) 78 | ..forward(); 79 | // Initial splash diameter is 60% of the target diameter, final 80 | // diameter is 10dps larger than the target diameter. 81 | 82 | // Controls the splash radius and its center. Starts upon confirm however its 83 | // Interval delays changes until the radius expansion has completed. 84 | _fadeOutController = AnimationController( 85 | duration: _kFadeOutDuration, vsync: controller.vsync) 86 | ..addListener(controller.markNeedsPaint) 87 | ..addStatusListener(_handleAlphaStatusChanged); 88 | _fadeOut = _fadeOutController.drive( 89 | IntTween( 90 | begin: color.alpha, 91 | end: 0, 92 | ), 93 | ); 94 | 95 | controller.addInkFeature(this); 96 | } 97 | 98 | final Offset _position; 99 | 100 | late AnimationController _radiusController; 101 | 102 | late Animation _fadeIn; 103 | late AnimationController _fadeInController; 104 | 105 | late Animation _fadeOut; 106 | late AnimationController _fadeOutController; 107 | 108 | // static final Animatable _fadeOutIntervalTween = CurveTween(curve: const Interval(_kFadeOutIntervalStart, 1.0)); 109 | 110 | @override 111 | void confirm() { 112 | _radiusController 113 | ..duration = _kRadiusDuration 114 | ..forward(); 115 | // This confirm may have been preceded by a cancel. 116 | _fadeInController.forward(); 117 | _fadeOutController.animateTo(1.0, duration: _kFadeOutDuration); 118 | } 119 | 120 | @override 121 | void cancel() { 122 | _fadeInController.stop(); 123 | // Watch out: setting _fadeOutController's value to 1.0 will 124 | // trigger a call to _handleAlphaStatusChanged() which will 125 | // dispose _fadeOutController. 126 | final double fadeOutValue = 1.0 - _fadeInController.value; 127 | _fadeOutController.value = fadeOutValue; 128 | if (fadeOutValue < 1.0) { 129 | _fadeOutController.animateTo(1.0, duration: _kCancelDuration); 130 | } 131 | } 132 | 133 | void _handleAlphaStatusChanged(AnimationStatus status) { 134 | if (status == AnimationStatus.completed) dispose(); 135 | } 136 | 137 | @override 138 | void dispose() { 139 | _radiusController.dispose(); 140 | _fadeInController.dispose(); 141 | _fadeOutController.dispose(); 142 | super.dispose(); 143 | } 144 | 145 | @override 146 | void paintFeature(Canvas canvas, Matrix4 transform) { 147 | final int alpha = 148 | _fadeInController.isAnimating ? _fadeIn.value : _fadeOut.value; 149 | final Paint paint = Paint()..color = color.withAlpha(alpha); 150 | // Splash moves to the center of the reference box. 151 | final Offset center = Offset.lerp( 152 | _position, 153 | referenceBox.size.center(Offset.zero), 154 | Curves.ease.transform(_radiusController.value), 155 | )!; 156 | final Offset centerUnder = Offset.lerp( 157 | _position.scale(2, 2), 158 | referenceBox.size.center(Offset.zero), 159 | Curves.ease.transform(_radiusController.value), 160 | )!; 161 | final Offset? originOffset = MatrixUtils.getAsTranslation(transform); 162 | canvas.save(); 163 | if (originOffset == null) { 164 | canvas.transform(transform.storage); 165 | } else { 166 | canvas.translate(originOffset.dx, originOffset.dy); 167 | } 168 | 169 | canvas.drawCircle(center, 50, paint); 170 | canvas.drawCircle(centerUnder, 100, paint); 171 | canvas.restore(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/src/utils/styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | import 'ripple.dart'; 5 | 6 | class Styles { 7 | static bool isDark = false; 8 | 9 | static ThemeData lightTheme = ThemeData( 10 | colorScheme: const ColorScheme.light( 11 | // surface: Colors.white, 12 | onSurface: Color(0xff222222), 13 | primary: Color(0xffce4a4f), 14 | onPrimary: Colors.white, 15 | primaryContainer: Color(0xffe07356), 16 | secondary: Color(0xff2b2540), 17 | secondaryContainer: Color(0xff483F6C), 18 | onSecondary: Colors.white, 19 | background: Color(0xfff3f6f9), 20 | onBackground: Color(0xff222222), 21 | ), 22 | shadowColor: const Color(0xff222222).withOpacity(0.05), 23 | textTheme: GoogleFonts.mulishTextTheme(), 24 | dividerColor: const Color(0xff6C6F8D), 25 | canvasColor: const Color(0x7fc3e8f3), 26 | scaffoldBackgroundColor: const Color(0xfff3f6f9), 27 | textButtonTheme: TextButtonThemeData( 28 | style: TextButton.styleFrom(splashFactory: const BookRippleFactory()), 29 | ), 30 | visualDensity: VisualDensity.adaptivePlatformDensity, 31 | ); 32 | 33 | static ThemeData darkTheme = ThemeData( 34 | colorScheme: const ColorScheme.dark( 35 | primary: Color(0xffce4a4f), 36 | onPrimary: Colors.white, 37 | secondary: Color(0xff917DD8), 38 | onSecondary: Colors.white, 39 | background: Color(0xff222222), 40 | onBackground: Color(0xfff3f6f9), 41 | ), 42 | textTheme: GoogleFonts.mulishTextTheme().apply( 43 | bodyColor: Colors.white, 44 | displayColor: Colors.white, 45 | decorationColor: Colors.white, 46 | ), 47 | hintColor: const Color(0xFFADADAD), 48 | shadowColor: const Color(0xff939393).withOpacity(0.05), 49 | dividerColor: const Color(0xff48445D), 50 | canvasColor: const Color(0x7f30393E), 51 | scaffoldBackgroundColor: const Color(0xff222222), 52 | visualDensity: VisualDensity.adaptivePlatformDensity, 53 | textButtonTheme: TextButtonThemeData( 54 | style: TextButton.styleFrom(splashFactory: const BookRippleFactory()), 55 | ), 56 | ); 57 | 58 | ThemeData get theme => isDark ? darkTheme : lightTheme; 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/utils/utils.dart: -------------------------------------------------------------------------------- 1 | export 'extensions.dart'; 2 | export 'radii.dart'; 3 | export 'ripple.dart'; 4 | export 'styles.dart'; 5 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutterbook 2 | description: A storyboard tool to accelerate the development of Flutter widgets. 3 | version: 0.1.9 4 | homepage: https://github.com/GhostWalker562/flutterbook 5 | 6 | environment: 7 | sdk: ">=3.0.5 <4.0.0" 8 | 9 | dependencies: 10 | cupertino_icons: ^1.0.3 11 | device_preview: ^1.1.0 12 | flutter_markdown: ^0.6.17+1 13 | dart_code_viewer2: ^1.0.2 14 | flutter: 15 | sdk: flutter 16 | flutter_feather_icons: ^2.0.0+1 17 | google_fonts: ^6.1.0 18 | provider: ^6.0.5 19 | recase: ^4.1.0 20 | 21 | dev_dependencies: 22 | flutter_lints: ^3.0.1 23 | flutter_test: 24 | sdk: flutter 25 | 26 | flutter: 27 | -------------------------------------------------------------------------------- /test/src/navigation/models/organizers_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutterbook/flutterbook.dart'; 4 | import 'package:flutterbook/src/routing/controls.dart'; 5 | 6 | void main() { 7 | group('Component State Path', () { 8 | final stub = (BuildContext context, ControlsInterface controls) { 9 | throw Exception('Unexpected build'); 10 | }; 11 | 12 | final baseState = ComponentState(stateName: 'My State', builder: stub); 13 | 14 | test('Standalone state', () { 15 | expect(baseState.path, 'my-state'); 16 | }); 17 | 18 | test('Component parent', () { 19 | final top = Component(componentName: 'Parent 1', states: [baseState]); 20 | expect(top.states.first.path, 'parent-1/my-state'); 21 | }); 22 | 23 | test('Folder parent', () { 24 | final folder = Folder( 25 | folderName: 'My Folder', 26 | organizers: [ 27 | Component(componentName: 'My Component', states: [baseState]), 28 | ], 29 | ); 30 | 31 | final component = folder.organizers.first as Component; 32 | expect(component.states.first.path, 'my-folder/my-component/my-state'); 33 | }); 34 | 35 | test('Category parent', () { 36 | final category = Category( 37 | categoryName: 'My Category', 38 | organizers: [ 39 | Folder( 40 | folderName: 'My Folder', 41 | organizers: [ 42 | Component(componentName: 'My Component', states: [baseState]), 43 | ], 44 | ), 45 | ], 46 | ); 47 | 48 | final component = category.organizers.first.organizers.first as Component; 49 | expect( 50 | component.states.first.path, 51 | 'my-category/my-folder/my-component/my-state', 52 | ); 53 | }); 54 | }); 55 | } 56 | --------------------------------------------------------------------------------