├── .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 |
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 |
10 |
11 | ---
12 |
13 | Go subscribe to my YouTube channel to support development.
14 |
15 | # [](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 |
--------------------------------------------------------------------------------