@ -0,0 +1,118 @@ |
||||
name: Report a bug |
||||
description: | |
||||
You found a bug in Flutter Quill causing your application to crash or |
||||
throw an exception, a widget is buggy, unexpected behavior or something looks wrong. |
||||
labels: 'bug' |
||||
body: |
||||
- type: markdown |
||||
attributes: |
||||
value: | |
||||
Thank you for using Flutter Quill! |
||||
|
||||
- type: checkboxes |
||||
attributes: |
||||
label: Is there an existing issue for this? |
||||
options: |
||||
- label: I have searched the [existing issues](https://github.com/singerdmx/flutter-quill/issues) |
||||
required: true |
||||
- type: input |
||||
attributes: |
||||
label: Flutter Quill version |
||||
description: Please tell us which version of `flutter_quill` that you are using. |
||||
placeholder: For example 9.0.0 |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Other Flutter Quill packages versions |
||||
description: If you are using any other packages like `flutter_quill_extensions` or `flutter_quill_test` please mention the versions here |
||||
placeholder: | |
||||
flutter_quill_extensions: ^0.6.10 |
||||
flutter_quill_test: ^0.0.5 |
||||
validations: |
||||
required: false |
||||
- type: textarea |
||||
attributes: |
||||
label: Steps to reproduce |
||||
description: Please tell us exactly how to reproduce the problem you are running into. |
||||
placeholder: | |
||||
1. ... |
||||
2. ... |
||||
3. ... |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Expected results |
||||
description: Please tell us what is expected to happen. |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Actual results |
||||
description: Please tell us what is actually happening. |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Code sample |
||||
description: | |
||||
Please create a minimal reproducible sample that shows the problem |
||||
and attach it below between the lines with the backticks. |
||||
|
||||
To create it, use the `flutter create bug` command and update the `main.dart` file. |
||||
|
||||
Alternatively, you can use https://dartpad.dev/ or create a public GitHub |
||||
repository to share your sample. |
||||
|
||||
Note: Please do not upload screenshots of text. Instead, use code blocks |
||||
or the above mentioned ways to upload your code sample. |
||||
value: | |
||||
<details><summary>Code sample</summary> |
||||
|
||||
```dart |
||||
[Paste your code here] |
||||
``` |
||||
|
||||
</details> |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Screenshots or Video |
||||
description: | |
||||
Upload any screenshots or video of the bug if applicable. |
||||
value: | |
||||
<details> |
||||
<summary>Screenshots / Video demonstration</summary> |
||||
|
||||
[Upload media here] |
||||
|
||||
</details> |
||||
validations: |
||||
required: false |
||||
- type: textarea |
||||
attributes: |
||||
label: Logs |
||||
description: | |
||||
Include the full logs of the commands you are running between the lines |
||||
with the backticks below. If you are running any `flutter` commands, |
||||
please include the output of running them with `--verbose`; for example, |
||||
the output of running `flutter --verbose create foo`. |
||||
|
||||
If the logs are too large to be uploaded to GitHub, you may upload |
||||
them as a `txt` file or use online tools like https://pastebin.com to |
||||
share it. |
||||
|
||||
Note: Please do not upload screenshots of text. Instead, use code blocks |
||||
or the above mentioned ways to upload logs. |
||||
value: | |
||||
<details><summary>Logs</summary> |
||||
|
||||
```console |
||||
[Paste your logs here] |
||||
``` |
||||
|
||||
</details> |
||||
validations: |
||||
required: false |
@ -0,0 +1,41 @@ |
||||
name: Feature request |
||||
description: Suggest a new idea for Flutter Quill. |
||||
labels: 'enhancement' |
||||
body: |
||||
- type: markdown |
||||
attributes: |
||||
value: | |
||||
Thank you for using Flutter Quill! |
||||
|
||||
- type: checkboxes |
||||
attributes: |
||||
label: Is there an existing issue for this? |
||||
description: Please search to see if an issue already exists for this feature request or proposal. |
||||
options: |
||||
- label: I have searched the [existing issues](https://github.com/singerdmx/flutter-quill/issues) |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Use case |
||||
description: | |
||||
Please tell us the problem you are running into that led to you wanting |
||||
a new feature. |
||||
|
||||
Is your feature request related to a problem? Please give a clear and |
||||
concise description of what the problem is. |
||||
|
||||
Describe the alternative solutions you've considered. Is there already a solution that solves this? |
||||
validations: |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: Proposal |
||||
description: | |
||||
Briefly but precisely describe what you would like Flutter Quill to be able to do. |
||||
|
||||
Consider attaching something showing what you are imagining: |
||||
* images |
||||
* videos |
||||
* code samples |
||||
validations: |
||||
required: true |
@ -0,0 +1,24 @@ |
||||
name: Ask a question |
||||
description: | |
||||
If you have any questions, feel free to ask |
||||
labels: 'help wanted' |
||||
body: |
||||
- type: markdown |
||||
attributes: |
||||
value: | |
||||
Thank you for using Flutter Quill! |
||||
|
||||
- type: checkboxes |
||||
attributes: |
||||
label: Is there an existing issue for this? |
||||
options: |
||||
- label: I have searched the [existing issues](https://github.com/singerdmx/flutter-quill/issues) |
||||
required: true |
||||
- type: textarea |
||||
attributes: |
||||
label: The question |
||||
description: Please explain your question here |
||||
placeholder: | |
||||
How do I save the images of Quill Editor? |
||||
validations: |
||||
required: true |
@ -1,25 +0,0 @@ |
||||
--- |
||||
name: Issue template |
||||
about: Common things to fill |
||||
title: "[Web] or [Mobile] or [Desktop]" |
||||
labels: '' |
||||
assignees: '' |
||||
|
||||
--- |
||||
|
||||
My issue is about [Web] |
||||
My issue is about [Mobile] |
||||
My issue is about [Desktop] |
||||
|
||||
I have tried running `example` directory successfully before creating an issue here. |
||||
|
||||
Please note that we are using <b>latest</b> flutter version in stable channel on branch master. If you are using beta or master channel, or you are not using <b>latest</b> flutter version in stable channel, you may experience error. |
||||
|
||||
<!-- Please explain how to encounter the issue in details if possible --> |
||||
|
||||
<!-- Don't forgot to mention the platform you are testing in --> |
||||
|
||||
<!-- Insert your images here if possible --> |
||||
<!-- Images: --> |
||||
|
||||
<!-- Add short video that showcase the problem will help --> |
@ -0,0 +1,38 @@ |
||||
name: Publish to pub.dev |
||||
|
||||
on: |
||||
push: |
||||
tags: |
||||
- 'v[0-9]+.[0-9]+.[0-9]+*' |
||||
|
||||
jobs: |
||||
publish: |
||||
permissions: |
||||
id-token: write |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
|
||||
- uses: subosito/flutter-action@v2 |
||||
with: |
||||
channel: 'stable' |
||||
|
||||
- name: Check flutter version |
||||
run: flutter --version |
||||
|
||||
# This is needed in order for the authentication to success |
||||
# pub token add https://pub.dev --env-var PUB_TOKEN |
||||
# Requests to "https://pub.dev" will now be authenticated using the secret token stored in the environment variable "PUB_TOKEN". |
||||
- uses: dart-lang/setup-dart@v1 |
||||
|
||||
- name: Install dependencies |
||||
run: flutter pub get |
||||
|
||||
# Here you can insert custom steps you need |
||||
# - run: dart tool/generate-code.dart |
||||
|
||||
- name: Re-generate the translations |
||||
run: ./scripts/regenerate-translations.sh |
||||
|
||||
- name: Publish |
||||
run: flutter pub publish --force |
@ -0,0 +1,18 @@ |
||||
name: 'Welcome New Contributors' |
||||
|
||||
on: |
||||
issues: |
||||
types: [opened] |
||||
pull_request_target: |
||||
types: [opened] |
||||
|
||||
jobs: |
||||
welcome-new-contributor: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: 'Greet the contributor' |
||||
uses: garg3133/welcome-new-contributors@v1.2 |
||||
with: |
||||
token: ${{ secrets.BOT_ACCESS_TOKEN }} |
||||
issue-message: 'Hello there, on behalf the Flutter Quill Team I would like to thank you for opening your first issue. Your inputs and insights are valuable in shaping a stable and reliable version for all our users. Thank you for being part of the open-source community!' |
||||
pr-message: 'Hi there, thanks for opening your first Pull Request to this project!!' |
@ -0,0 +1,13 @@ |
||||
# For local development |
||||
pubspec_overrides.yaml |
||||
pubspec_overrides.yaml.disabled |
||||
|
||||
# Github |
||||
.github/ |
||||
|
||||
# The example |
||||
example/.fvm/ |
||||
example/build/ |
||||
example/.dart_tool/ |
||||
|
||||
scripts/ |
@ -0,0 +1,64 @@ |
||||
# Contributing |
||||
|
||||
The contributions are more than welcome! <br> |
||||
This project will be better with the open-source community help |
||||
|
||||
You can check the [Todo](./doc/todo.md) list if you want to |
||||
|
||||
There are no guidelines for now. |
||||
This page will be updated in the future. |
||||
|
||||
## Steps to contributing |
||||
|
||||
You will need a GitHub account as well as Git installed and configured with your GitHub account on your machine |
||||
|
||||
1. Fork the repository in GitHub |
||||
2. clone the forked repository using `git` |
||||
3. Add the `upstream` repository using: |
||||
``` |
||||
git remote add upstream git@github.com:singerdmx/flutter-quill.git |
||||
``` |
||||
4. Open the project with your favorite IDE, usually, we prefer to use Jetbrains IDEs, but since [VS Code](https://code.visualstudio.com) is more used and has more support for Dart, then we suggest using it if you want to. |
||||
5. Create a new git branch and switch to it using: |
||||
|
||||
``` |
||||
git checkout -b your-branch-name |
||||
``` |
||||
The `your-branch-name` is your choice |
||||
6. Make your changes |
||||
7. If you are working on changes that depend on different libraries in the same repo, then in that directory copy `pubspec_overrides.yaml.disabled` which exists in all the libraries (`flutter_quill_test` and `flutter_quill_extensions` etc...) |
||||
to `pubspec_overrides.yaml` which will be ignored by `.gitignore` and will be used by dart pub to override the libraries |
||||
``` |
||||
cp pubspec_overrides.yaml.disabled pubspec_overrides.yaml |
||||
``` |
||||
or save some time with the following script: |
||||
``` |
||||
./scripts/enable_local_dev.sh |
||||
``` |
||||
8. Test them in the [example](./example) and add changes in there if necessary |
||||
9. Mention the new changes in the [CHANGELOG.md](../CHANGELOG.md) in the next block |
||||
10. Run the following script if possible |
||||
``` |
||||
./scripts/before-push.sh |
||||
``` |
||||
11. When you are done sending your pull request, run: |
||||
``` |
||||
git add . |
||||
git commit -m "Your commit message" |
||||
git push origin your-branch-name |
||||
``` |
||||
this will push the new branch to your forked repository |
||||
12. Now you can send your pull request either by following the link that you will get in the command line or open your |
||||
forked repository. You will find an option to send the pull request, you can also |
||||
open the [Pull Requests](https://github.com/singerdmx/flutter-quill) tab and send new pull request |
||||
13. Please wait for the review, and we might ask you to make more changes, then run: |
||||
``` |
||||
git add . |
||||
git commit -m "Your new commit message" |
||||
git push origin your-branch-name |
||||
``` |
||||
|
||||
Thank you for your time and efforts in this open-source community project!! |
||||
|
||||
## Development Notes |
||||
Please read the [Development Notes](./doc/development_notes.md) as they are important while development |
@ -0,0 +1,43 @@ |
||||
# Custom `QuillToolbar` Buttons |
||||
|
||||
You may add custom buttons to the _end_ of the toolbar, via the `customButtons` option, which is a `List` of `QuillToolbarCustomButtonOptions`. |
||||
|
||||
To add an Icon, we should use a new `QuillToolbarCustomButtonOptions` class |
||||
|
||||
```dart |
||||
QuillToolbarCustomButtonOptions( |
||||
icon: const Icon(Icons.ac_unit), |
||||
tooltip: '', |
||||
onPressed: () {}, |
||||
afterButtonPressed: () {}, |
||||
), |
||||
``` |
||||
|
||||
Each `QuillCustomButton` is used as part of the `customButtons` option as follows: |
||||
|
||||
```dart |
||||
QuillToolbar( |
||||
configurations: QuillToolbarConfigurations( |
||||
customButtons: [ |
||||
QuillToolbarCustomButtonOptions( |
||||
icon: const Icon(Icons.ac_unit), |
||||
onPressed: () { |
||||
debugPrint('snowflake1'); |
||||
}, |
||||
), |
||||
QuillToolbarCustomButtonOptions( |
||||
icon: const Icon(Icons.ac_unit), |
||||
onPressed: () { |
||||
debugPrint('snowflake2'); |
||||
}, |
||||
), |
||||
QuillToolbarCustomButtonOptions( |
||||
icon: const Icon(Icons.ac_unit), |
||||
onPressed: () { |
||||
debugPrint('snowflake3'); |
||||
}, |
||||
), |
||||
], |
||||
), |
||||
), |
||||
``` |
@ -0,0 +1,15 @@ |
||||
# Font Size |
||||
|
||||
Within the editor toolbar, a drop-down with font-sizing capabilities is available. This can be enabled or disabled with `showFontSize`. |
||||
|
||||
When enabled, the default font-size values can be modified via _optional_ `fontSizeValues`. `fontSizeValues` accepts a `Map<String, String>` consisting of a `String` title for the font size and a `String` value for the font size. Example: |
||||
|
||||
```dart |
||||
fontSizeValues: const {'Small': '8', 'Medium': '24.5', 'Large': '46'} |
||||
``` |
||||
|
||||
Font size can be cleared with a value of `0`, for example: |
||||
|
||||
```dart |
||||
fontSizeValues: const {'Small': '8', 'Medium': '24.5', 'Large': '46', 'Clear': '0'} |
||||
``` |
@ -0,0 +1,25 @@ |
||||
# Localizations Setup |
||||
in addition to the required delegatess which mentioned above in [Using custom app widget](./using_custom_app_widget.md) |
||||
|
||||
which are: |
||||
```dart |
||||
localizationsDelegates: const [ |
||||
DefaultCupertinoLocalizations.delegate, |
||||
DefaultMaterialLocalizations.delegate, |
||||
DefaultWidgetsLocalizations.delegate, |
||||
], |
||||
``` |
||||
which are used by offical flutter widgets |
||||
|
||||
The library also needs the |
||||
```dart |
||||
// Required localizations delegates ... |
||||
FlutterQuillLocalizations.delegate |
||||
``` |
||||
|
||||
To offer the default localizations. |
||||
|
||||
But **you don't have to** since we have wraped the `QuillEditor` and `QuillToolbar` with `FlutterQuillLocalizationsWidget` which will check if it sets then it will go, if not, then it will be provided only for them, so it's not really required, but if you are overriding the `localizationsDelegates` you could also add the `FlutterQuillLocalizations.delegate` |
||||
which won't change anything |
||||
|
||||
There are additional notes in the [Translation](../translation.md) section |
@ -0,0 +1,34 @@ |
||||
# Using Custom App Widget |
||||
|
||||
This project use some adaptive widgets like `AdaptiveTextSelectionToolbar` which require the following delegates: |
||||
|
||||
1. Default Material Localizations delegate |
||||
2. Default Cupertino Localizations delegate |
||||
3. Defualt Widgets Localizations delegate |
||||
|
||||
You don't need to include those since there are defined by default |
||||
but if you are using Custom app or you are overriding the `localizationsDelegates` in the App widget |
||||
then please make sure it's including those: |
||||
|
||||
```dart |
||||
localizationsDelegates: const [ |
||||
DefaultCupertinoLocalizations.delegate, |
||||
DefaultMaterialLocalizations.delegate, |
||||
DefaultWidgetsLocalizations.delegate, |
||||
], |
||||
``` |
||||
|
||||
And you might need more depending on your use case, for example if you are using custom localizations for your app, using custom app widget like `FluentApp` from [FluentUI] |
||||
which will also need |
||||
|
||||
```dart |
||||
localizationsDelegates: const [ |
||||
// Required localizations delegates ... |
||||
FluentLocalizations.delegate, |
||||
AppLocalizations.delegate, |
||||
], |
||||
``` |
||||
|
||||
Note: In the latest versions of `FluentApp` you no longer need to add the `localizationsDelegates` but this is just an example, for more [info](https://github.com/bdlukaa/fluent_ui/pull/946) |
||||
|
||||
There are additonal notes in [Localizations](./localizations_setup.md) page |
@ -0,0 +1,124 @@ |
||||
# Custom Embed Blocks |
||||
|
||||
Sometimes you want to add some custom content inside your text, custom widgets inside of them. An example is adding notes to the text, or anything custom that you want to add in your text editor. |
||||
|
||||
The only thing that you need is to add a `CustomBlockEmbed` and provider a builder for it to the `embedBuilders` parameter, to transform the data inside of the Custom Block into a widget! |
||||
|
||||
Here is an example: |
||||
|
||||
Starting with the `CustomBlockEmbed`, here we extend it and add the methods that are useful for the 'Note' widget, that will be the `Document`, used by the `flutter_quill` to render the rich text. |
||||
|
||||
```dart |
||||
class NotesBlockEmbed extends CustomBlockEmbed { |
||||
const NotesBlockEmbed(String value) : super(noteType, value); |
||||
|
||||
static const String noteType = 'notes'; |
||||
|
||||
static NotesBlockEmbed fromDocument(Document document) => |
||||
NotesBlockEmbed(jsonEncode(document.toDelta().toJson())); |
||||
|
||||
Document get document => Document.fromJson(jsonDecode(data)); |
||||
} |
||||
``` |
||||
|
||||
After that, we need to map this "notes" type into a widget. In that case, I used a `ListTile` with a text to show the plain text resume of the note, and the `onTap` function to edit the note. |
||||
Don't forget to add this method to the `QuillEditor` after that! |
||||
|
||||
```dart |
||||
class NotesEmbedBuilder extends EmbedBuilder { |
||||
NotesEmbedBuilder({required this.addEditNote}); |
||||
|
||||
Future<void> Function(BuildContext context, {Document? document}) addEditNote; |
||||
|
||||
@override |
||||
String get key => 'notes'; |
||||
|
||||
@override |
||||
Widget build( |
||||
BuildContext context, |
||||
QuillController controller, |
||||
Embed node, |
||||
bool readOnly, |
||||
bool inline, |
||||
TextStyle textStyle, |
||||
) { |
||||
final notes = NotesBlockEmbed(node.value.data).document; |
||||
|
||||
return Material( |
||||
color: Colors.transparent, |
||||
child: ListTile( |
||||
title: Text( |
||||
notes.toPlainText().replaceAll('\n', ' '), |
||||
maxLines: 3, |
||||
overflow: TextOverflow.ellipsis, |
||||
), |
||||
leading: const Icon(Icons.notes), |
||||
onTap: () => addEditNote(context, document: notes), |
||||
shape: RoundedRectangleBorder( |
||||
borderRadius: BorderRadius.circular(10), |
||||
side: const BorderSide(color: Colors.grey), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
And finally, we write the function to add/edit this note. The `showDialog` function shows the QuillEditor to edit the note, after the user ends the edition, we check if the document has something, and if it has, we add or edit the `NotesBlockEmbed` inside of a `BlockEmbed.custom` (this is a little detail that will not work if you don't pass the `CustomBlockEmbed` inside of a `BlockEmbed.custom`). |
||||
|
||||
```dart |
||||
Future<void> _addEditNote(BuildContext context, {Document? document}) async { |
||||
final isEditing = document != null; |
||||
final quillEditorController = QuillController( |
||||
document: document ?? Document(), |
||||
selection: const TextSelection.collapsed(offset: 0), |
||||
); |
||||
|
||||
await showDialog( |
||||
context: context, |
||||
builder: (context) => AlertDialog( |
||||
titlePadding: const EdgeInsets.only(left: 16, top: 8), |
||||
title: Row( |
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
children: [ |
||||
Text('${isEditing ? 'Edit' : 'Add'} note'), |
||||
IconButton( |
||||
onPressed: () => Navigator.of(context).pop(), |
||||
icon: const Icon(Icons.close), |
||||
) |
||||
], |
||||
), |
||||
content: QuillEditor.basic( |
||||
controller: quillEditorController, |
||||
readOnly: false, |
||||
), |
||||
), |
||||
); |
||||
|
||||
if (quillEditorController.document.isEmpty()) return; |
||||
|
||||
final block = BlockEmbed.custom( |
||||
NotesBlockEmbed.fromDocument(quillEditorController.document), |
||||
); |
||||
final controller = _controller!; |
||||
final index = controller.selection.baseOffset; |
||||
final length = controller.selection.extentOffset - index; |
||||
|
||||
if (isEditing) { |
||||
final offset = getEmbedNode(controller, controller.selection.start).offset; |
||||
controller.replaceText( |
||||
offset, 1, block, TextSelection.collapsed(offset: offset)); |
||||
} else { |
||||
controller.replaceText(index, length, block, null); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
And voila, we have a custom widget inside of the rich text editor! |
||||
|
||||
<p float="left"> |
||||
<img width="400" alt="1" src="https://i.imgur.com/yBTPYeS.png"> |
||||
</p> |
||||
|
||||
> 1. For more info and a video example, see the [PR of this feature](https://github.com/singerdmx/flutter-quill/pull/877) |
||||
> 2. For more details, check out [this YouTube video](https://youtu.be/pI5p5j7cfHc) |
@ -0,0 +1,143 @@ |
||||
# Custom Toolbar |
||||
|
||||
If you want to use custom toolbar but still want the support of this libray |
||||
You can use the `QuillBaseToolbar` which is the base for the `QuillToolbar` |
||||
|
||||
> If you are using the toolbar buttons like `QuillToolbarHistoryButton`, `QuillToolbarToggleStyleButton` in the somewhere like the the custom toolbar then you must provide them with `QuillToolbarProvider` inherited widget, you don't have to do this if you are using the `QuillToolbar` since it will be done for you |
||||
|
||||
Example: |
||||
|
||||
```dart |
||||
QuillProvider( |
||||
configurations: QuillConfigurations( |
||||
controller: _controller, |
||||
sharedConfigurations: const QuillSharedConfigurations(), |
||||
), |
||||
child: Column( |
||||
children: [ |
||||
QuillToolbarProvider( |
||||
toolbarConfigurations: const QuillToolbarConfigurations(), |
||||
child: QuillBaseToolbar( |
||||
configurations: QuillBaseToolbarConfigurations( |
||||
toolbarSize: 15 * 2, |
||||
multiRowsDisplay: false, |
||||
childrenBuilder: (context) { |
||||
final controller = context.requireQuillController; |
||||
return [ |
||||
QuillToolbarHistoryButton( |
||||
controller: controller, |
||||
options: const QuillToolbarHistoryButtonOptions( |
||||
isUndo: true), |
||||
), |
||||
QuillToolbarHistoryButton( |
||||
controller: controller, |
||||
options: const QuillToolbarHistoryButtonOptions( |
||||
isUndo: false), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.bold, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_bold, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.italic, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_italic, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.underline, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_underline, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarClearFormatButton( |
||||
controller: controller, |
||||
options: const QuillToolbarClearFormatButtonOptions( |
||||
iconData: Icons.format_clear, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
VerticalDivider( |
||||
indent: 12, |
||||
endIndent: 12, |
||||
color: Colors.grey.shade400, |
||||
), |
||||
QuillToolbarSelectHeaderStyleButtons( |
||||
controller: controller, |
||||
options: |
||||
const QuillToolbarSelectHeaderStyleButtonsOptions( |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.ol, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_list_numbered, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.ul, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_list_bulleted, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.blockQuote, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_quote, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
VerticalDivider( |
||||
indent: 12, |
||||
endIndent: 12, |
||||
color: Colors.grey.shade400, |
||||
), |
||||
QuillToolbarIndentButton( |
||||
controller: controller, |
||||
isIncrease: true, |
||||
options: const QuillToolbarIndentButtonOptions( |
||||
iconData: Icons.format_indent_increase, |
||||
iconSize: 20, |
||||
)), |
||||
QuillToolbarIndentButton( |
||||
controller: controller, |
||||
isIncrease: false, |
||||
options: const QuillToolbarIndentButtonOptions( |
||||
iconData: Icons.format_indent_decrease, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
]; |
||||
}, |
||||
), |
||||
), |
||||
), |
||||
Expanded( |
||||
child: QuillEditor.basic( |
||||
configurations: const QuillEditorConfigurations( |
||||
readOnly: false, |
||||
placeholder: 'Write your notes', |
||||
padding: EdgeInsets.all(16), |
||||
), |
||||
), |
||||
) |
||||
], |
||||
), |
||||
) |
||||
``` |
||||
|
||||
if you want more customized toolbar feel free to create your own and use the `controller` to interact with the editor. checkout the `QuillToolbar` and the buttons inside it to see an example of how that will works |
@ -0,0 +1,3 @@ |
||||
# Development notes |
||||
|
||||
- When update the translations or localizations in the app, please take a look at the [Translation](./translation.md) page as it have important notes in order to work, if you also adding a feature that add new localizations then you need to the instructions of it in order for the translations to take affect |
@ -0,0 +1,48 @@ |
||||
# Todo |
||||
|
||||
This is a todo list page that added recently and will be updated soon. |
||||
|
||||
## Table of contents |
||||
- [Todo](#todo) |
||||
- [Table of contents](#table-of-contents) |
||||
- [Flutter Quill](#flutter-quill) |
||||
- [Features](#features) |
||||
- [Improvemenets](#improvemenets) |
||||
- [Bugs](#bugs) |
||||
- [Flutter Quill Extensions](#flutter-quill-extensions) |
||||
- [Features](#features-1) |
||||
- [Improvemenets](#improvemenets-1) |
||||
- [Bugs](#bugs-1) |
||||
|
||||
## Flutter Quill |
||||
|
||||
### Features |
||||
|
||||
- Add support for Text magnification feature, for more [info](https://github.com/singerdmx/flutter-quill/issues/1504) |
||||
- Provide a way to expose quills undo redo stacks, for more [info](https://github.com/singerdmx/flutter-quill/issues/1381) |
||||
- Add callback to the `QuillToolbarColorButton` for custom color picking logic |
||||
|
||||
### Improvemenets |
||||
|
||||
- Improve the Raw Quill Editor, for more [info](https://github.com/singerdmx/flutter-quill/issues/1509) |
||||
- Provide more support to all the platforms |
||||
- Extract the shared properties between `QuillRawEditorConfigurations` and `QuillEditorConfigurations` |
||||
|
||||
### Bugs |
||||
|
||||
Empty for now. |
||||
Please go to the [issues](https://github.com/singerdmx/flutter-quill/issues) |
||||
|
||||
|
||||
## Flutter Quill Extensions |
||||
|
||||
### Features |
||||
- Add support for copying images to the Clipboard |
||||
|
||||
### Improvemenets |
||||
|
||||
Please check the todos, this list will be updated soon. |
||||
|
||||
### Bugs |
||||
|
||||
Please check the todos, this list will be updated soon. |
@ -0,0 +1,65 @@ |
||||
# Translation |
||||
|
||||
The package offers translations for the quill toolbar and editor, it will follow the locale that is defined in your `WidgetsApp` for example `MaterialApp` which usually follow the system local and it unless you set your own locale with: |
||||
|
||||
```dart |
||||
QuillProvider( |
||||
configurations: QuillConfigurations( |
||||
controller: _controller, |
||||
sharedConfigurations: const QuillSharedConfigurations( |
||||
locale: Locale('fr'), // will take affect only if FlutterQuillLocalizations.delegate is not defined in the Widget app |
||||
), |
||||
), |
||||
child: Column( |
||||
children: [ |
||||
const QuillToolbar( |
||||
configurations: QuillToolbarConfigurations(), |
||||
), |
||||
Expanded( |
||||
child: QuillEditor.basic( |
||||
configurations: const QuillEditorConfigurations(), |
||||
), |
||||
) |
||||
], |
||||
), |
||||
) |
||||
``` |
||||
|
||||
Currently, translations are available for these 31 locales: |
||||
|
||||
* `Locale('en')`, `Locale('ar')`, `Locale('bn')`, `Locale('bs')` |
||||
* `Locale('cs')`, `Locale('de')`, `Locale('da')`, `Locale('fr')` |
||||
* `Locale('he')`, `Locale('zh', 'CN')`, `Locale('zh', 'HK')`, `Locale('ko')` |
||||
* `Locale('ru')`, `Locale('es')`, `Locale('tk')`, `Locale('tr')` |
||||
* `Locale('uk')`, `Locale('ur')`, `Locale('pt')`, `Locale('pl')` |
||||
* `Locale('vi')`, `Locale('id')`, `Locale('it')`, `Locale('ms')` |
||||
* `Locale('nl')`, `Locale('no')`, `Locale('fa')`, `Locale('hi')` |
||||
* `Locale('sr')`, `Locale('sw')`, `Locale('ja')` |
||||
|
||||
#### Contributing to translations |
||||
|
||||
The translation files is located at [l10n folder](../lib/src/l10n/). Feel free to contribute your own translations, just copy the [English translations](../lib/src/l10n/quill_en.arb) map and replace the values with your translations. |
||||
|
||||
Add new file in the l10n folder with the following name |
||||
`quill_${localName}.arb` for example `quill_de.arb` |
||||
|
||||
paste the English version and replace the values |
||||
|
||||
Also you can take a look at the [untranslated.json](../lib/src/l10n/untranslated.json) json file, which is a generated file that tell you which keys with which locales hasn't translated so you can find the missings easily |
||||
|
||||
After you are done and want to test the changes, run the following in the root folder (preferred): |
||||
|
||||
``` |
||||
flutter gen-l10n |
||||
``` |
||||
|
||||
or: |
||||
|
||||
``` |
||||
./scripts/regenerate-translations.sh |
||||
``` |
||||
|
||||
|
||||
This will generate the new dart files from the arb files in order to take affect, otherwise you won't notice a difference |
||||
|
||||
Then open a pull request so everyone can benefit from your translations! |
@ -1,16 +1,18 @@ |
||||
# app |
||||
# Demo |
||||
|
||||
demo app |
||||
This is just a demo of Flutter Quill |
||||
|
||||
## Getting Started |
||||
|
||||
This project is a starting point for a Flutter application. |
||||
## Screenshots |
||||
|
||||
A few resources to get you started if this is your first Flutter project: |
||||
<img src="./assets/images/screenshot_1.png" width="150" alt="Screenshot 1"> |
||||
<img src="./assets/images/screenshot_2.png" width="150" alt="Screenshot 2"> |
||||
<img src="./assets/images/screenshot_3.png" width="150" alt="Screenshot 3"> |
||||
<img src="./assets/images/screenshot_4.png" width="150" alt="Screenshot 4"> |
||||
|
||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) |
||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) |
||||
## Development notes |
||||
|
||||
For help getting started with Flutter, view our |
||||
[online documentation](https://flutter.dev/docs), which offers tutorials, |
||||
samples, guidance on mobile development, and a full API reference. |
||||
- When changing the `assets` please run: |
||||
``` |
||||
dart run build_runner build --delete-conflicting-outputs |
||||
``` |
@ -0,0 +1,37 @@ |
||||
include: package:flutter_lints/flutter.yaml |
||||
|
||||
analyzer: |
||||
errors: |
||||
invalid_annotation_target: ignore |
||||
|
||||
linter: |
||||
rules: |
||||
always_declare_return_types: true |
||||
always_put_required_named_parameters_first: true |
||||
annotate_overrides: true |
||||
avoid_empty_else: true |
||||
avoid_escaping_inner_quotes: true |
||||
avoid_print: false |
||||
avoid_redundant_argument_values: false |
||||
avoid_types_on_closure_parameters: true |
||||
avoid_void_async: true |
||||
cascade_invocations: true |
||||
directives_ordering: true |
||||
omit_local_variable_types: true |
||||
prefer_const_constructors: true |
||||
prefer_const_constructors_in_immutables: true |
||||
prefer_const_declarations: true |
||||
prefer_final_fields: true |
||||
prefer_final_in_for_each: true |
||||
prefer_final_locals: true |
||||
prefer_initializing_formals: true |
||||
prefer_int_literals: true |
||||
prefer_interpolation_to_compose_strings: true |
||||
prefer_relative_imports: true |
||||
prefer_single_quotes: true |
||||
sort_constructors_first: true |
||||
sort_unnamed_constructors_first: true |
||||
unnecessary_lambdas: true |
||||
unnecessary_parenthesis: true |
||||
unnecessary_string_interpolations: true |
||||
library_private_types_in_public_api: false |
@ -1,8 +1,7 @@ |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="com.example.app"> |
||||
<!-- Flutter needs it to communicate with the running application |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!-- The INTERNET permission is required for development. Specifically, |
||||
the Flutter tool needs it to communicate with the running application |
||||
to allow setting breakpoints, to provide hot reload, etc. |
||||
--> |
||||
<uses-permission android:name="android.permission.INTERNET"/> |
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> |
||||
</manifest> |
||||
|
@ -1,6 +1,5 @@ |
||||
package com.example.app |
||||
package com.example.example |
||||
|
||||
import io.flutter.embedding.android.FlutterActivity |
||||
|
||||
class MainActivity: FlutterActivity() { |
||||
} |
||||
class MainActivity: FlutterActivity() |
||||
|
@ -0,0 +1,4 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
<string name="app_name">Flutter Quill Demo</string> |
||||
</resources> |
@ -1,8 +1,7 @@ |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="com.example.app"> |
||||
<!-- Flutter needs it to communicate with the running application |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!-- The INTERNET permission is required for development. Specifically, |
||||
the Flutter tool needs it to communicate with the running application |
||||
to allow setting breakpoints, to provide hot reload, etc. |
||||
--> |
||||
<uses-permission android:name="android.permission.INTERNET"/> |
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> |
||||
</manifest> |
||||
|
@ -1,3 +1,6 @@ |
||||
org.gradle.jvmargs=-Xmx1536M |
||||
android.useAndroidX=true |
||||
android.enableJetifier=true |
||||
android.defaults.buildfeatures.buildconfig=true |
||||
android.nonTransitiveRClass=false |
||||
android.nonFinalResIds=false |
||||
|
@ -1,6 +1,5 @@ |
||||
#Fri Jun 23 08:50:38 CEST 2017 |
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip |
||||
|
@ -1,11 +1,20 @@ |
||||
include ':app' |
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties") |
||||
pluginManagement { |
||||
def flutterSdkPath = { |
||||
def properties = new Properties() |
||||
|
||||
assert localPropertiesFile.exists() |
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } |
||||
|
||||
file("local.properties").withInputStream { properties.load(it) } |
||||
def flutterSdkPath = properties.getProperty("flutter.sdk") |
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" |
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" |
||||
return flutterSdkPath |
||||
} |
||||
settings.ext.flutterSdkPath = flutterSdkPath() |
||||
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") |
||||
|
||||
plugins { |
||||
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false |
||||
} |
||||
} |
||||
|
||||
include ":app" |
||||
|
||||
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" |
||||
|
After Width: | Height: | Size: 401 KiB |
After Width: | Height: | Size: 348 KiB |
After Width: | Height: | Size: 342 KiB |
After Width: | Height: | Size: 279 KiB |
@ -1,521 +0,0 @@ |
||||
[ |
||||
{ |
||||
"insert": "Flutter Quill" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"header": 1 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\nRich text editor for Flutter" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"header": 2 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Quill component for Flutter" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"header": 3 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "This " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"italic": true, |
||||
"background": "transparent" |
||||
}, |
||||
"insert": "library" |
||||
}, |
||||
{ |
||||
"insert": " supports " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"bold": true, |
||||
"background": "#ebd6ff" |
||||
}, |
||||
"insert": "mobile" |
||||
}, |
||||
{ |
||||
"insert": " platform " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"underline": true, |
||||
"bold": true, |
||||
"color": "#e60000" |
||||
}, |
||||
"insert": "only" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"color": "rgba(0, 0, 0, 0.847)" |
||||
}, |
||||
"insert": " and " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"strike": true, |
||||
"color": "black" |
||||
}, |
||||
"insert": "web" |
||||
}, |
||||
{ |
||||
"insert": " is not supported.\nYou are welcome to use " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"link": "https://bulletjournal.us/home/index.html" |
||||
}, |
||||
"insert": "Bullet Journal" |
||||
}, |
||||
{ |
||||
"insert": ":\nTrack personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Check out what you and your teammates are working on each day" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\nSplitting bills with friends can never be easier." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Start creating a group and invite your friends to join." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Create a BuJo of Ledger type to see expense or balance summary." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\nAttach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s)." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"blockquote": true |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\nvar BuJo = 'Bullet' + 'Journal'" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"code-block": true |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\nStart tracking in your browser" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Stop the timer on your phone" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "All your time entries are synced" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "between the phone apps" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "and the website." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 3 |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "\nCenter Align" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"align": "center" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Right Align" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"align": "right" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Justify Align" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"align": "justify" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Have trouble finding things? " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Just type in the search bar" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "and easily find contents" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "across projects or folders." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "It matches text in your note or task." |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Enable reminders so that you will get notified by" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "email" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "message on your phone" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "popup on the web site" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "ordered" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Create a BuJo serving as project or folder" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Organize your" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "tasks" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2, |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "notes" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2, |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "transactions" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 2, |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "under BuJo " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 3, |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "See them in Calendar" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "or hierarchical view" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"indent": 1, |
||||
"list": "bullet" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "this is a check list" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "checked" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "this is a uncheck list" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"list": "unchecked" |
||||
}, |
||||
"insert": "\n" |
||||
}, |
||||
{ |
||||
"insert": "Font " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"font": "sans-serif" |
||||
}, |
||||
"insert": "Sans Serif" |
||||
}, |
||||
{ |
||||
"insert": " " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"font": "serif" |
||||
}, |
||||
"insert": "Serif" |
||||
}, |
||||
{ |
||||
"insert": " " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"font": "monospace" |
||||
}, |
||||
"insert": "Monospace" |
||||
}, |
||||
{ |
||||
"insert": " Size " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"size": "small" |
||||
}, |
||||
"insert": "Small" |
||||
}, |
||||
{ |
||||
"insert": " " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"size": "large" |
||||
}, |
||||
"insert": "Large" |
||||
}, |
||||
{ |
||||
"insert": " " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"size": "huge" |
||||
}, |
||||
"insert": "Huge" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"size": "15.0" |
||||
}, |
||||
"insert": "font size 15" |
||||
}, |
||||
{ |
||||
"insert": " " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"size": "35" |
||||
}, |
||||
"insert": "font size 35" |
||||
}, |
||||
{ |
||||
"insert": " " |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"size": "20" |
||||
}, |
||||
"insert": "font size 20" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"token": "built_in" |
||||
}, |
||||
"insert": " diff" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"token": "operator" |
||||
}, |
||||
"insert": "-match" |
||||
}, |
||||
{ |
||||
"attributes": { |
||||
"token": "literal" |
||||
}, |
||||
"insert": "-patch" |
||||
}, |
||||
{ |
||||
"insert": { |
||||
"image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" |
||||
}, |
||||
"attributes": { |
||||
"width": "230", |
||||
"style": "display: block; margin: auto;" |
||||
} |
||||
}, |
||||
{ |
||||
"insert": "\n" |
||||
} |
||||
] |
@ -1,6 +0,0 @@ |
||||
#import <Flutter/Flutter.h> |
||||
#import <UIKit/UIKit.h> |
||||
|
||||
@interface AppDelegate : FlutterAppDelegate |
||||
|
||||
@end |
@ -1,13 +0,0 @@ |
||||
#import "AppDelegate.h" |
||||
#import "GeneratedPluginRegistrant.h" |
||||
|
||||
@implementation AppDelegate |
||||
|
||||
- (BOOL)application:(UIApplication *)application |
||||
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
||||
[GeneratedPluginRegistrant registerWithRegistry:self]; |
||||
// Override point for customization after application launch. |
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions]; |
||||
} |
||||
|
||||
@end |
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,9 +0,0 @@ |
||||
#import <Flutter/Flutter.h> |
||||
#import <UIKit/UIKit.h> |
||||
#import "AppDelegate.h" |
||||
|
||||
int main(int argc, char* argv[]) { |
||||
@autoreleasepool { |
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
import Flutter |
||||
import UIKit |
||||
import XCTest |
||||
|
||||
class RunnerTests: XCTestCase { |
||||
|
||||
func testExample() { |
||||
// If you add code to the Runner application, consider adding tests here. |
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest. |
||||
} |
||||
|
||||
} |
@ -0,0 +1,114 @@ |
||||
/// GENERATED CODE - DO NOT MODIFY BY HAND |
||||
/// ***************************************************** |
||||
/// FlutterGen |
||||
/// ***************************************************** |
||||
|
||||
// coverage:ignore-file |
||||
// ignore_for_file: type=lint |
||||
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use |
||||
|
||||
import 'package:flutter/widgets.dart'; |
||||
|
||||
class $AssetsImagesGen { |
||||
const $AssetsImagesGen(); |
||||
|
||||
/// File path: assets/images/screenshot_1.png |
||||
AssetGenImage get screenshot1 => |
||||
const AssetGenImage('assets/images/screenshot_1.png'); |
||||
|
||||
/// File path: assets/images/screenshot_2.png |
||||
AssetGenImage get screenshot2 => |
||||
const AssetGenImage('assets/images/screenshot_2.png'); |
||||
|
||||
/// File path: assets/images/screenshot_3.png |
||||
AssetGenImage get screenshot3 => |
||||
const AssetGenImage('assets/images/screenshot_3.png'); |
||||
|
||||
/// File path: assets/images/screenshot_4.png |
||||
AssetGenImage get screenshot4 => |
||||
const AssetGenImage('assets/images/screenshot_4.png'); |
||||
|
||||
/// List of all assets |
||||
List<AssetGenImage> get values => |
||||
[screenshot1, screenshot2, screenshot3, screenshot4]; |
||||
} |
||||
|
||||
class Assets { |
||||
Assets._(); |
||||
|
||||
static const $AssetsImagesGen images = $AssetsImagesGen(); |
||||
} |
||||
|
||||
class AssetGenImage { |
||||
const AssetGenImage(this._assetName); |
||||
|
||||
final String _assetName; |
||||
|
||||
Image image({ |
||||
Key? key, |
||||
AssetBundle? bundle, |
||||
ImageFrameBuilder? frameBuilder, |
||||
ImageErrorWidgetBuilder? errorBuilder, |
||||
String? semanticLabel, |
||||
bool excludeFromSemantics = false, |
||||
double? scale, |
||||
double? width, |
||||
double? height, |
||||
Color? color, |
||||
Animation<double>? opacity, |
||||
BlendMode? colorBlendMode, |
||||
BoxFit? fit, |
||||
AlignmentGeometry alignment = Alignment.center, |
||||
ImageRepeat repeat = ImageRepeat.noRepeat, |
||||
Rect? centerSlice, |
||||
bool matchTextDirection = false, |
||||
bool gaplessPlayback = false, |
||||
bool isAntiAlias = false, |
||||
String? package, |
||||
FilterQuality filterQuality = FilterQuality.low, |
||||
int? cacheWidth, |
||||
int? cacheHeight, |
||||
}) { |
||||
return Image.asset( |
||||
_assetName, |
||||
key: key, |
||||
bundle: bundle, |
||||
frameBuilder: frameBuilder, |
||||
errorBuilder: errorBuilder, |
||||
semanticLabel: semanticLabel, |
||||
excludeFromSemantics: excludeFromSemantics, |
||||
scale: scale, |
||||
width: width, |
||||
height: height, |
||||
color: color, |
||||
opacity: opacity, |
||||
colorBlendMode: colorBlendMode, |
||||
fit: fit, |
||||
alignment: alignment, |
||||
repeat: repeat, |
||||
centerSlice: centerSlice, |
||||
matchTextDirection: matchTextDirection, |
||||
gaplessPlayback: gaplessPlayback, |
||||
isAntiAlias: isAntiAlias, |
||||
package: package, |
||||
filterQuality: filterQuality, |
||||
cacheWidth: cacheWidth, |
||||
cacheHeight: cacheHeight, |
||||
); |
||||
} |
||||
|
||||
ImageProvider provider({ |
||||
AssetBundle? bundle, |
||||
String? package, |
||||
}) { |
||||
return AssetImage( |
||||
_assetName, |
||||
bundle: bundle, |
||||
package: package, |
||||
); |
||||
} |
||||
|
||||
String get path => _assetName; |
||||
|
||||
String get keyName => _assetName; |
||||
} |
@ -0,0 +1,39 @@ |
||||
/// GENERATED CODE - DO NOT MODIFY BY HAND |
||||
/// ***************************************************** |
||||
/// FlutterGen |
||||
/// ***************************************************** |
||||
|
||||
// coverage:ignore-file |
||||
// ignore_for_file: type=lint |
||||
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use |
||||
|
||||
class FontFamily { |
||||
FontFamily._(); |
||||
|
||||
/// Font family: SF-UI-Display |
||||
static const String sFUIDisplay = 'SF-UI-Display'; |
||||
|
||||
/// Font family: ibarra-real-nova |
||||
static const String ibarraRealNova = 'ibarra-real-nova'; |
||||
|
||||
/// Font family: monospace |
||||
static const String monospace = 'monospace'; |
||||
|
||||
/// Font family: nunito |
||||
static const String nunito = 'nunito'; |
||||
|
||||
/// Font family: pacifico |
||||
static const String pacifico = 'pacifico'; |
||||
|
||||
/// Font family: roboto-mono |
||||
static const String robotoMono = 'roboto-mono'; |
||||
|
||||
/// Font family: sans-serif |
||||
static const String sansSerif = 'sans-serif'; |
||||
|
||||
/// Font family: serif |
||||
static const String serif = 'serif'; |
||||
|
||||
/// Font family: square-peg |
||||
static const String squarePeg = 'square-peg'; |
||||
} |
@ -0,0 +1 @@ |
||||
|
@ -1,33 +1,155 @@ |
||||
import 'package:flutter/foundation.dart' show kIsWeb; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_localizations/flutter_localizations.dart'; |
||||
import 'package:flutter_bloc/flutter_bloc.dart'; |
||||
import 'package:flutter_localizations/flutter_localizations.dart' |
||||
show |
||||
GlobalCupertinoLocalizations, |
||||
GlobalMaterialLocalizations, |
||||
GlobalWidgetsLocalizations; |
||||
import 'package:flutter_quill/flutter_quill.dart' show Document; |
||||
import 'package:flutter_quill/translations.dart' show FlutterQuillLocalizations; |
||||
import 'package:hydrated_bloc/hydrated_bloc.dart' |
||||
show HydratedBloc, HydratedStorage; |
||||
import 'package:path_provider/path_provider.dart' |
||||
show getApplicationDocumentsDirectory; |
||||
|
||||
import 'pages/home_page.dart'; |
||||
import 'presentation/home/widgets/home_screen.dart'; |
||||
import 'presentation/quill/quill_screen.dart'; |
||||
import 'presentation/quill/samples/quill_default_sample.dart'; |
||||
import 'presentation/quill/samples/quill_images_sample.dart'; |
||||
import 'presentation/quill/samples/quill_text_sample.dart'; |
||||
import 'presentation/quill/samples/quill_videos_sample.dart'; |
||||
import 'presentation/settings/cubit/settings_cubit.dart'; |
||||
import 'presentation/settings/widgets/settings_screen.dart'; |
||||
|
||||
void main() { |
||||
void main() async { |
||||
WidgetsFlutterBinding.ensureInitialized(); |
||||
runApp(MyApp()); |
||||
HydratedBloc.storage = await HydratedStorage.build( |
||||
storageDirectory: kIsWeb |
||||
? HydratedStorage.webStorageDirectory |
||||
: await getApplicationDocumentsDirectory(), |
||||
); |
||||
runApp(const MyApp()); |
||||
} |
||||
|
||||
class MyApp extends StatelessWidget { |
||||
const MyApp({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return MultiBlocProvider( |
||||
providers: [ |
||||
BlocProvider( |
||||
create: (context) => SettingsCubit(), |
||||
), |
||||
], |
||||
child: BlocBuilder<SettingsCubit, SettingsState>( |
||||
builder: (context, state) { |
||||
return MaterialApp( |
||||
title: 'Flutter Quill Demo', |
||||
theme: ThemeData.light(useMaterial3: true), |
||||
darkTheme: ThemeData.dark(useMaterial3: true), |
||||
themeMode: state.themeMode, |
||||
debugShowCheckedModeBanner: false, |
||||
title: 'Quill Demo', |
||||
theme: ThemeData( |
||||
primarySwatch: Colors.blue, |
||||
visualDensity: VisualDensity.adaptivePlatformDensity, |
||||
), |
||||
localizationsDelegates: [ |
||||
localizationsDelegates: const [ |
||||
GlobalMaterialLocalizations.delegate, |
||||
GlobalWidgetsLocalizations.delegate, |
||||
GlobalCupertinoLocalizations.delegate, |
||||
// FlutterQuillLocalizations.delegate, |
||||
], |
||||
supportedLocales: [ |
||||
const Locale('en', 'US'), |
||||
const Locale('zh', 'HK'), |
||||
], |
||||
home: HomePage(), |
||||
supportedLocales: FlutterQuillLocalizations.supportedLocales, |
||||
routes: { |
||||
SettingsScreen.routeName: (context) => const SettingsScreen(), |
||||
}, |
||||
onGenerateRoute: (settings) { |
||||
final name = settings.name; |
||||
if (name == HomeScreen.routeName) { |
||||
return MaterialPageRoute( |
||||
builder: (context) { |
||||
return const HomeScreen(); |
||||
}, |
||||
); |
||||
} |
||||
if (name == QuillScreen.routeName) { |
||||
return MaterialPageRoute( |
||||
builder: (context) { |
||||
final args = settings.arguments as QuillScreenArgs; |
||||
return QuillScreen( |
||||
args: args, |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
return null; |
||||
}, |
||||
onUnknownRoute: (settings) { |
||||
return MaterialPageRoute( |
||||
builder: (context) => Scaffold( |
||||
appBar: AppBar( |
||||
title: const Text('Not found'), |
||||
), |
||||
body: const Text('404'), |
||||
), |
||||
); |
||||
}, |
||||
home: Builder( |
||||
builder: (context) { |
||||
final screen = switch (state.defaultScreen) { |
||||
DefaultScreen.home => const HomeScreen(), |
||||
DefaultScreen.settings => const SettingsScreen(), |
||||
DefaultScreen.imagesSample => QuillScreen( |
||||
args: QuillScreenArgs( |
||||
document: Document.fromJson(quillImagesSample), |
||||
), |
||||
), |
||||
DefaultScreen.videosSample => QuillScreen( |
||||
args: QuillScreenArgs( |
||||
document: Document.fromJson(quillVideosSample), |
||||
), |
||||
), |
||||
DefaultScreen.textSample => QuillScreen( |
||||
args: QuillScreenArgs( |
||||
document: Document.fromJson(quillTextSample), |
||||
), |
||||
), |
||||
DefaultScreen.emptySample => QuillScreen( |
||||
args: QuillScreenArgs( |
||||
document: Document(), |
||||
), |
||||
), |
||||
DefaultScreen.defaultSample => QuillScreen( |
||||
args: QuillScreenArgs( |
||||
document: Document.fromJson(quillDefaultSample), |
||||
), |
||||
), |
||||
}; |
||||
return AnimatedSwitcher( |
||||
duration: const Duration(milliseconds: 330), |
||||
transitionBuilder: (child, animation) { |
||||
// This animation is from flutter.dev example |
||||
const begin = Offset(0, 1); |
||||
const end = Offset.zero; |
||||
const curve = Curves.ease; |
||||
|
||||
final tween = Tween( |
||||
begin: begin, |
||||
end: end, |
||||
).chain( |
||||
CurveTween(curve: curve), |
||||
); |
||||
|
||||
return SlideTransition( |
||||
position: animation.drive(tween), |
||||
child: child, |
||||
); |
||||
}, |
||||
child: screen, |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
||||
|
@ -1,564 +0,0 @@ |
||||
// ignore_for_file: avoid_redundant_argument_values |
||||
|
||||
import 'dart:async'; |
||||
import 'dart:convert'; |
||||
import 'dart:io' show File, Platform; |
||||
import 'dart:ui'; |
||||
|
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'package:filesystem_picker/filesystem_picker.dart'; |
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
import 'package:flutter_quill/extensions.dart'; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; |
||||
import 'package:path/path.dart' as path; |
||||
import 'package:path_provider/path_provider.dart'; |
||||
|
||||
import '../universal_ui/universal_ui.dart'; |
||||
import '../widgets/time_stamp_embed_widget.dart'; |
||||
import 'read_only_page.dart'; |
||||
|
||||
enum _SelectionType { |
||||
none, |
||||
word, |
||||
// line, |
||||
} |
||||
|
||||
class HomePage extends StatefulWidget { |
||||
@override |
||||
_HomePageState createState() => _HomePageState(); |
||||
} |
||||
|
||||
class _HomePageState extends State<HomePage> { |
||||
late final QuillController _controller; |
||||
late final Future<void> _loadDocumentFromAssetsFuture; |
||||
final FocusNode _focusNode = FocusNode(); |
||||
Timer? _selectAllTimer; |
||||
_SelectionType _selectionType = _SelectionType.none; |
||||
|
||||
@override |
||||
void dispose() { |
||||
_selectAllTimer?.cancel(); |
||||
// Dispose the controller to free resources |
||||
_controller.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_loadDocumentFromAssetsFuture = _loadFromAssets(); |
||||
} |
||||
|
||||
Future<void> _loadFromAssets() async { |
||||
try { |
||||
final result = await rootBundle.loadString(isDesktop() |
||||
? 'assets/sample_data_nomedia.json' |
||||
: 'assets/sample_data.json'); |
||||
final doc = Document.fromJson(jsonDecode(result)); |
||||
_controller = QuillController( |
||||
document: doc, |
||||
selection: const TextSelection.collapsed(offset: 0), |
||||
); |
||||
} catch (error) { |
||||
final doc = Document()..insert(0, 'Empty asset'); |
||||
_controller = QuillController( |
||||
document: doc, |
||||
selection: const TextSelection.collapsed(offset: 0), |
||||
); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return FutureBuilder( |
||||
future: _loadDocumentFromAssetsFuture, |
||||
builder: (context, snapshot) { |
||||
if (snapshot.connectionState == ConnectionState.waiting) { |
||||
return const Scaffold( |
||||
body: Center(child: CircularProgressIndicator.adaptive()), |
||||
); |
||||
} |
||||
return Scaffold( |
||||
appBar: AppBar( |
||||
backgroundColor: Colors.grey.shade800, |
||||
elevation: 0, |
||||
centerTitle: false, |
||||
title: const Text( |
||||
'Flutter Quill', |
||||
), |
||||
actions: [ |
||||
IconButton( |
||||
onPressed: () => _insertTimeStamp( |
||||
_controller, |
||||
DateTime.now().toString(), |
||||
), |
||||
icon: const Icon(Icons.add_alarm_rounded), |
||||
), |
||||
IconButton( |
||||
onPressed: () => showDialog( |
||||
context: context, |
||||
builder: (context) => AlertDialog( |
||||
content: Text(_controller.document.toPlainText([ |
||||
...FlutterQuillEmbeds.builders(), |
||||
TimeStampEmbedBuilderWidget() |
||||
])), |
||||
), |
||||
), |
||||
icon: const Icon(Icons.text_fields_rounded), |
||||
) |
||||
], |
||||
), |
||||
drawer: Container( |
||||
constraints: BoxConstraints( |
||||
maxWidth: MediaQuery.sizeOf(context).width * 0.7), |
||||
color: Colors.grey.shade800, |
||||
child: _buildMenuBar(context), |
||||
), |
||||
body: _buildWelcomeEditor(context), |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
|
||||
bool _onTripleClickSelection() { |
||||
final controller = _controller; |
||||
|
||||
_selectAllTimer?.cancel(); |
||||
_selectAllTimer = null; |
||||
|
||||
// If you want to select all text after paragraph, uncomment this line |
||||
// if (_selectionType == _SelectionType.line) { |
||||
// final selection = TextSelection( |
||||
// baseOffset: 0, |
||||
// extentOffset: controller.document.length, |
||||
// ); |
||||
|
||||
// controller.updateSelection(selection, ChangeSource.REMOTE); |
||||
|
||||
// _selectionType = _SelectionType.none; |
||||
|
||||
// return true; |
||||
// } |
||||
|
||||
if (controller.selection.isCollapsed) { |
||||
_selectionType = _SelectionType.none; |
||||
} |
||||
|
||||
if (_selectionType == _SelectionType.none) { |
||||
_selectionType = _SelectionType.word; |
||||
_startTripleClickTimer(); |
||||
return false; |
||||
} |
||||
|
||||
if (_selectionType == _SelectionType.word) { |
||||
final child = controller.document.queryChild( |
||||
controller.selection.baseOffset, |
||||
); |
||||
final offset = child.node?.documentOffset ?? 0; |
||||
final length = child.node?.length ?? 0; |
||||
|
||||
final selection = TextSelection( |
||||
baseOffset: offset, |
||||
extentOffset: offset + length, |
||||
); |
||||
|
||||
controller.updateSelection(selection, ChangeSource.REMOTE); |
||||
|
||||
// _selectionType = _SelectionType.line; |
||||
|
||||
_selectionType = _SelectionType.none; |
||||
|
||||
_startTripleClickTimer(); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
void _startTripleClickTimer() { |
||||
_selectAllTimer = Timer(const Duration(milliseconds: 900), () { |
||||
_selectionType = _SelectionType.none; |
||||
}); |
||||
} |
||||
|
||||
QuillEditor get quillEditor { |
||||
if (kIsWeb) { |
||||
return QuillEditor( |
||||
focusNode: _focusNode, |
||||
scrollController: ScrollController(), |
||||
configurations: QuillEditorConfigurations( |
||||
placeholder: 'Add content', |
||||
readOnly: false, |
||||
scrollable: true, |
||||
autoFocus: false, |
||||
expands: false, |
||||
padding: EdgeInsets.zero, |
||||
onTapUp: (details, p1) { |
||||
return _onTripleClickSelection(); |
||||
}, |
||||
customStyles: const DefaultStyles( |
||||
h1: DefaultTextBlockStyle( |
||||
TextStyle( |
||||
fontSize: 32, |
||||
color: Colors.black, |
||||
height: 1.15, |
||||
fontWeight: FontWeight.w300, |
||||
), |
||||
VerticalSpacing(16, 0), |
||||
VerticalSpacing(0, 0), |
||||
null), |
||||
sizeSmall: TextStyle(fontSize: 9), |
||||
), |
||||
embedBuilders: [ |
||||
...defaultEmbedBuildersWeb, |
||||
TimeStampEmbedBuilderWidget() |
||||
], |
||||
), |
||||
); |
||||
} |
||||
return QuillEditor( |
||||
configurations: QuillEditorConfigurations( |
||||
placeholder: 'Add content', |
||||
readOnly: false, |
||||
autoFocus: false, |
||||
enableSelectionToolbar: isMobile(), |
||||
expands: false, |
||||
padding: EdgeInsets.zero, |
||||
onImagePaste: _onImagePaste, |
||||
onTapUp: (details, p1) { |
||||
return _onTripleClickSelection(); |
||||
}, |
||||
customStyles: const DefaultStyles( |
||||
h1: DefaultTextBlockStyle( |
||||
TextStyle( |
||||
fontSize: 32, |
||||
color: Colors.black, |
||||
height: 1.15, |
||||
fontWeight: FontWeight.w300, |
||||
), |
||||
VerticalSpacing(16, 0), |
||||
VerticalSpacing(0, 0), |
||||
null), |
||||
sizeSmall: TextStyle(fontSize: 9), |
||||
subscript: TextStyle( |
||||
fontFamily: 'SF-UI-Display', |
||||
fontFeatures: [FontFeature.subscripts()], |
||||
), |
||||
superscript: TextStyle( |
||||
fontFamily: 'SF-UI-Display', |
||||
fontFeatures: [FontFeature.superscripts()], |
||||
), |
||||
), |
||||
embedBuilders: [ |
||||
...FlutterQuillEmbeds.builders(), |
||||
TimeStampEmbedBuilderWidget() |
||||
], |
||||
), |
||||
scrollController: ScrollController(), |
||||
focusNode: _focusNode, |
||||
); |
||||
} |
||||
|
||||
QuillToolbar get quillToolbar { |
||||
if (kIsWeb) { |
||||
return QuillToolbar( |
||||
configurations: QuillToolbarConfigurations( |
||||
embedButtons: FlutterQuillEmbeds.buttons( |
||||
onImagePickCallback: _onImagePickCallback, |
||||
webImagePickImpl: _webImagePickImpl, |
||||
), |
||||
buttonOptions: QuillToolbarButtonOptions( |
||||
base: QuillToolbarBaseButtonOptions( |
||||
afterButtonPressed: _focusNode.requestFocus, |
||||
), |
||||
), |
||||
), |
||||
// afterButtonPressed: _focusNode.requestFocus, |
||||
); |
||||
} |
||||
if (_isDesktop()) { |
||||
return QuillToolbar( |
||||
configurations: QuillToolbarConfigurations( |
||||
embedButtons: FlutterQuillEmbeds.buttons( |
||||
onImagePickCallback: _onImagePickCallback, |
||||
filePickImpl: openFileSystemPickerForDesktop, |
||||
), |
||||
showAlignmentButtons: true, |
||||
buttonOptions: QuillToolbarButtonOptions( |
||||
base: QuillToolbarBaseButtonOptions( |
||||
afterButtonPressed: _focusNode.requestFocus, |
||||
), |
||||
), |
||||
), |
||||
// afterButtonPressed: _focusNode.requestFocus, |
||||
); |
||||
} |
||||
return QuillToolbar( |
||||
configurations: QuillToolbarConfigurations( |
||||
embedButtons: FlutterQuillEmbeds.buttons( |
||||
// provide a callback to enable picking images from device. |
||||
// if omit, "image" button only allows adding images from url. |
||||
// same goes for videos. |
||||
onImagePickCallback: _onImagePickCallback, |
||||
onVideoPickCallback: _onVideoPickCallback, |
||||
// uncomment to provide a custom "pick from" dialog. |
||||
// mediaPickSettingSelector: _selectMediaPickSetting, |
||||
// uncomment to provide a custom "pick from" dialog. |
||||
// cameraPickSettingSelector: _selectCameraPickSetting, |
||||
), |
||||
showAlignmentButtons: true, |
||||
buttonOptions: QuillToolbarButtonOptions( |
||||
base: QuillToolbarBaseButtonOptions( |
||||
afterButtonPressed: _focusNode.requestFocus, |
||||
), |
||||
), |
||||
), |
||||
// afterButtonPressed: _focusNode.requestFocus, |
||||
); |
||||
} |
||||
|
||||
Widget _buildWelcomeEditor(BuildContext context) { |
||||
// BUG in web!! should not releated to this pull request |
||||
/// |
||||
///══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═════════════════════ |
||||
///══════════════════════════════════════ |
||||
// The following bool object was thrown building MediaQuery |
||||
//(MediaQueryData(size: Size(769.0, 1205.0), |
||||
// devicePixelRatio: 1.0, textScaleFactor: 1.0, platformBrightness: |
||||
//Brightness.dark, padding: |
||||
// EdgeInsets.zero, viewPadding: EdgeInsets.zero, viewInsets: |
||||
// EdgeInsets.zero, |
||||
// systemGestureInsets: |
||||
// EdgeInsets.zero, alwaysUse24HourFormat: false, accessibleNavigation: |
||||
// false, |
||||
// highContrast: false, |
||||
// disableAnimations: false, invertColors: false, boldText: false, |
||||
//navigationMode: traditional, |
||||
// gestureSettings: DeviceGestureSettings(touchSlop: null), displayFeatures: |
||||
// [] |
||||
// )): |
||||
// false |
||||
// The relevant error-causing widget was: |
||||
// SafeArea |
||||
/// |
||||
/// |
||||
return SafeArea( |
||||
child: QuillProvider( |
||||
configurations: QuillConfigurations( |
||||
controller: _controller, |
||||
), |
||||
child: Column( |
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
children: <Widget>[ |
||||
Expanded( |
||||
flex: 15, |
||||
child: Container( |
||||
color: Colors.white, |
||||
padding: const EdgeInsets.only(left: 16, right: 16), |
||||
child: quillEditor, |
||||
), |
||||
), |
||||
kIsWeb |
||||
? Expanded( |
||||
child: Container( |
||||
padding: |
||||
const EdgeInsets.symmetric(vertical: 16, horizontal: 8), |
||||
child: quillToolbar, |
||||
)) |
||||
: Container( |
||||
child: quillToolbar, |
||||
) |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
bool _isDesktop() => !kIsWeb && !Platform.isAndroid && !Platform.isIOS; |
||||
|
||||
Future<String?> openFileSystemPickerForDesktop(BuildContext context) async { |
||||
return await FilesystemPicker.open( |
||||
context: context, |
||||
rootDirectory: await getApplicationDocumentsDirectory(), |
||||
fsType: FilesystemType.file, |
||||
fileTileSelectMode: FileTileSelectMode.wholeTile, |
||||
); |
||||
} |
||||
|
||||
// Renders the image picked by imagePicker from local file storage |
||||
// You can also upload the picked image to any server (eg : AWS s3 |
||||
// or Firebase) and then return the uploaded image URL. |
||||
Future<String> _onImagePickCallback(File file) async { |
||||
// Copies the picked file from temporary cache to applications directory |
||||
final appDocDir = await getApplicationDocumentsDirectory(); |
||||
final copiedFile = |
||||
await file.copy('${appDocDir.path}/${path.basename(file.path)}'); |
||||
return copiedFile.path.toString(); |
||||
} |
||||
|
||||
Future<String?> _webImagePickImpl( |
||||
OnImagePickCallback onImagePickCallback) async { |
||||
final result = await FilePicker.platform.pickFiles(); |
||||
if (result == null) { |
||||
return null; |
||||
} |
||||
|
||||
// Take first, because we don't allow picking multiple files. |
||||
final fileName = result.files.first.name; |
||||
final file = File(fileName); |
||||
|
||||
return onImagePickCallback(file); |
||||
} |
||||
|
||||
// Renders the video picked by imagePicker from local file storage |
||||
// You can also upload the picked video to any server (eg : AWS s3 |
||||
// or Firebase) and then return the uploaded video URL. |
||||
Future<String> _onVideoPickCallback(File file) async { |
||||
// Copies the picked file from temporary cache to applications directory |
||||
final appDocDir = await getApplicationDocumentsDirectory(); |
||||
final copiedFile = |
||||
await file.copy('${appDocDir.path}/${path.basename(file.path)}'); |
||||
return copiedFile.path.toString(); |
||||
} |
||||
|
||||
// ignore: unused_element |
||||
Future<MediaPickSetting?> _selectMediaPickSetting(BuildContext context) => |
||||
showDialog<MediaPickSetting>( |
||||
context: context, |
||||
builder: (ctx) => AlertDialog( |
||||
contentPadding: EdgeInsets.zero, |
||||
content: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
TextButton.icon( |
||||
icon: const Icon(Icons.collections), |
||||
label: const Text('Gallery'), |
||||
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery), |
||||
), |
||||
TextButton.icon( |
||||
icon: const Icon(Icons.link), |
||||
label: const Text('Link'), |
||||
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link), |
||||
) |
||||
], |
||||
), |
||||
), |
||||
); |
||||
|
||||
// ignore: unused_element |
||||
Future<MediaPickSetting?> _selectCameraPickSetting(BuildContext context) => |
||||
showDialog<MediaPickSetting>( |
||||
context: context, |
||||
builder: (ctx) => AlertDialog( |
||||
contentPadding: EdgeInsets.zero, |
||||
content: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
TextButton.icon( |
||||
icon: const Icon(Icons.camera), |
||||
label: const Text('Capture a photo'), |
||||
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Camera), |
||||
), |
||||
TextButton.icon( |
||||
icon: const Icon(Icons.video_call), |
||||
label: const Text('Capture a video'), |
||||
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Video), |
||||
) |
||||
], |
||||
), |
||||
), |
||||
); |
||||
|
||||
Widget _buildMenuBar(BuildContext context) { |
||||
final size = MediaQuery.sizeOf(context); |
||||
const itemStyle = TextStyle( |
||||
color: Colors.white, |
||||
fontSize: 18, |
||||
fontWeight: FontWeight.bold, |
||||
); |
||||
return Column( |
||||
mainAxisAlignment: MainAxisAlignment.center, |
||||
children: [ |
||||
Divider( |
||||
thickness: 2, |
||||
color: Colors.white, |
||||
indent: size.width * 0.1, |
||||
endIndent: size.width * 0.1, |
||||
), |
||||
ListTile( |
||||
title: const Center(child: Text('Read only demo', style: itemStyle)), |
||||
dense: true, |
||||
visualDensity: VisualDensity.compact, |
||||
onTap: _readOnly, |
||||
), |
||||
Divider( |
||||
thickness: 2, |
||||
color: Colors.white, |
||||
indent: size.width * 0.1, |
||||
endIndent: size.width * 0.1, |
||||
), |
||||
], |
||||
); |
||||
} |
||||
|
||||
void _readOnly() { |
||||
Navigator.pop(super.context); |
||||
Navigator.push( |
||||
super.context, |
||||
MaterialPageRoute( |
||||
builder: (context) => ReadOnlyPage(), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Future<String> _onImagePaste(Uint8List imageBytes) async { |
||||
// Saves the image to applications directory |
||||
final appDocDir = await getApplicationDocumentsDirectory(); |
||||
final file = await File( |
||||
'${appDocDir.path}/${path.basename('${DateTime.now().millisecondsSinceEpoch}.png')}', |
||||
).writeAsBytes(imageBytes, flush: true); |
||||
return file.path.toString(); |
||||
} |
||||
|
||||
static void _insertTimeStamp(QuillController controller, String string) { |
||||
controller.document.insert(controller.selection.extentOffset, '\n'); |
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.LOCAL, |
||||
); |
||||
|
||||
controller.document.insert( |
||||
controller.selection.extentOffset, |
||||
TimeStampEmbed(string), |
||||
); |
||||
|
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.LOCAL, |
||||
); |
||||
|
||||
controller.document.insert(controller.selection.extentOffset, ' '); |
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.LOCAL, |
||||
); |
||||
|
||||
controller.document.insert(controller.selection.extentOffset, '\n'); |
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.LOCAL, |
||||
); |
||||
} |
||||
} |
@ -1,81 +0,0 @@ |
||||
// ignore_for_file: avoid_redundant_argument_values |
||||
|
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/extensions.dart'; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; |
||||
|
||||
import '../universal_ui/universal_ui.dart'; |
||||
import '../widgets/demo_scaffold.dart'; |
||||
|
||||
class ReadOnlyPage extends StatefulWidget { |
||||
@override |
||||
_ReadOnlyPageState createState() => _ReadOnlyPageState(); |
||||
} |
||||
|
||||
class _ReadOnlyPageState extends State<ReadOnlyPage> { |
||||
final FocusNode _focusNode = FocusNode(); |
||||
|
||||
bool _edit = false; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return DemoScaffold( |
||||
documentFilename: isDesktop() |
||||
? 'assets/sample_data_nomedia.json' |
||||
: 'sample_data_nomedia.json', |
||||
builder: _buildContent, |
||||
showToolbar: _edit == true, |
||||
floatingActionButton: FloatingActionButton.extended( |
||||
label: Text(_edit == true ? 'Done' : 'Edit'), |
||||
onPressed: _toggleEdit, |
||||
icon: Icon(_edit == true ? Icons.check : Icons.edit), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildContent(BuildContext context, QuillController? controller) { |
||||
var quillEditor = QuillEditor( |
||||
configurations: QuillEditorConfigurations( |
||||
expands: false, |
||||
padding: EdgeInsets.zero, |
||||
embedBuilders: FlutterQuillEmbeds.builders(), |
||||
scrollable: true, |
||||
autoFocus: true, |
||||
), |
||||
scrollController: ScrollController(), |
||||
focusNode: _focusNode, |
||||
// readOnly: !_edit, |
||||
); |
||||
if (kIsWeb) { |
||||
quillEditor = QuillEditor( |
||||
configurations: QuillEditorConfigurations( |
||||
autoFocus: true, |
||||
expands: false, |
||||
padding: EdgeInsets.zero, |
||||
embedBuilders: defaultEmbedBuildersWeb, |
||||
scrollable: true, |
||||
), |
||||
scrollController: ScrollController(), |
||||
focusNode: _focusNode, |
||||
); |
||||
} |
||||
return Padding( |
||||
padding: const EdgeInsets.all(8), |
||||
child: Container( |
||||
decoration: BoxDecoration( |
||||
color: Colors.white, |
||||
border: Border.all(color: Colors.grey.shade200), |
||||
), |
||||
child: quillEditor, |
||||
), |
||||
); |
||||
} |
||||
|
||||
void _toggleEdit() { |
||||
setState(() { |
||||
_edit = !_edit; |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
import 'package:flutter/material.dart' |
||||
show ScaffoldMessenger, ScaffoldMessengerState, SnackBar; |
||||
import 'package:flutter/widgets.dart' show BuildContext, Text; |
||||
|
||||
extension ScaffoldMessengerStateExt on ScaffoldMessengerState { |
||||
void showText(String text) { |
||||
showSnackBar(SnackBar(content: Text(text))); |
||||
} |
||||
} |
||||
|
||||
extension BuildContextExt on BuildContext { |
||||
ScaffoldMessengerState get messenger => ScaffoldMessenger.of(this); |
||||
} |
@ -0,0 +1,53 @@ |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
class HomeScreenExampleItem extends StatelessWidget { |
||||
const HomeScreenExampleItem({ |
||||
required this.title, |
||||
required this.icon, |
||||
required this.text, |
||||
required this.onPressed, |
||||
super.key, |
||||
}); |
||||
final String title; |
||||
final Widget icon; |
||||
final String text; |
||||
final VoidCallback onPressed; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Column( |
||||
children: [ |
||||
SizedBox( |
||||
height: 200, |
||||
width: double.infinity, |
||||
child: GestureDetector( |
||||
onTap: onPressed, |
||||
child: Card( |
||||
child: SingleChildScrollView( |
||||
child: Column( |
||||
children: [ |
||||
const SizedBox(height: 2), |
||||
Text( |
||||
title, |
||||
style: Theme.of(context).textTheme.titleLarge, |
||||
), |
||||
const SizedBox(height: 8), |
||||
icon, |
||||
const SizedBox(height: 8), |
||||
Padding( |
||||
padding: const EdgeInsets.all(8), |
||||
child: Text( |
||||
text, |
||||
style: Theme.of(context).textTheme.bodyMedium, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,188 @@ |
||||
import 'dart:convert' show jsonDecode; |
||||
|
||||
import 'package:cross_file/cross_file.dart'; |
||||
import 'package:file_picker/file_picker.dart' show FilePicker, FileType; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
|
||||
import '../../extensions/scaffold_messenger.dart'; |
||||
import '../../quill/quill_screen.dart'; |
||||
import '../../quill/samples/quill_default_sample.dart'; |
||||
import '../../quill/samples/quill_images_sample.dart'; |
||||
import '../../quill/samples/quill_text_sample.dart'; |
||||
import '../../quill/samples/quill_videos_sample.dart'; |
||||
import '../../settings/widgets/settings_screen.dart'; |
||||
import 'example_item.dart'; |
||||
|
||||
class HomeScreen extends StatelessWidget { |
||||
const HomeScreen({super.key}); |
||||
|
||||
static const routeName = '/home'; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Scaffold( |
||||
appBar: AppBar( |
||||
title: const Text('Flutter Quill Demo'), |
||||
), |
||||
drawer: Drawer( |
||||
child: ListView( |
||||
children: [ |
||||
const DrawerHeader( |
||||
child: Text( |
||||
'Flutter Quill Demo', |
||||
), |
||||
), |
||||
ListTile( |
||||
title: const Text('Settings'), |
||||
leading: const Icon(Icons.settings), |
||||
onTap: () { |
||||
Navigator.of(context) |
||||
..pop() |
||||
..pushNamed(SettingsScreen.routeName); |
||||
}, |
||||
), |
||||
], |
||||
), |
||||
), |
||||
body: SafeArea( |
||||
child: Column( |
||||
children: [ |
||||
SizedBox( |
||||
width: double.infinity, |
||||
child: Text( |
||||
'Welcome to Flutter Quill Demo!', |
||||
style: Theme.of(context).textTheme.titleLarge, |
||||
textAlign: TextAlign.center, |
||||
), |
||||
), |
||||
Expanded( |
||||
child: ListView( |
||||
padding: const EdgeInsets.all(16), |
||||
children: [ |
||||
HomeScreenExampleItem( |
||||
title: 'Default', |
||||
icon: const Icon( |
||||
Icons.home, |
||||
size: 50, |
||||
), |
||||
text: |
||||
'If you want to see how the editor work with default content, ' |
||||
'see any samples or you are working on it', |
||||
onPressed: () => Navigator.of(context).pushNamed( |
||||
QuillScreen.routeName, |
||||
arguments: QuillScreenArgs( |
||||
document: Document.fromJson(quillDefaultSample), |
||||
), |
||||
), |
||||
), |
||||
const SizedBox(height: 4), |
||||
HomeScreenExampleItem( |
||||
title: 'Images', |
||||
icon: const Icon( |
||||
Icons.image, |
||||
size: 50, |
||||
), |
||||
text: 'If you want to see how the editor work with images, ' |
||||
'see any samples or you are working on it', |
||||
onPressed: () => Navigator.of(context).pushNamed( |
||||
QuillScreen.routeName, |
||||
arguments: QuillScreenArgs( |
||||
document: Document.fromJson(quillImagesSample), |
||||
), |
||||
), |
||||
), |
||||
const SizedBox(height: 4), |
||||
HomeScreenExampleItem( |
||||
title: 'Videos', |
||||
icon: const Icon( |
||||
Icons.video_chat, |
||||
size: 50, |
||||
), |
||||
text: 'If you want to see how the editor work with videos, ' |
||||
'see any samples or you are working on it', |
||||
onPressed: () => Navigator.of(context).pushNamed( |
||||
QuillScreen.routeName, |
||||
arguments: QuillScreenArgs( |
||||
document: Document.fromJson(quillVideosSample), |
||||
), |
||||
), |
||||
), |
||||
HomeScreenExampleItem( |
||||
title: 'Text', |
||||
icon: const Icon( |
||||
Icons.edit_document, |
||||
size: 50, |
||||
), |
||||
text: 'If you want to see how the editor work with text, ' |
||||
'see any samples or you are working on it', |
||||
onPressed: () => Navigator.of(context).pushNamed( |
||||
QuillScreen.routeName, |
||||
arguments: QuillScreenArgs( |
||||
document: Document.fromJson(quillTextSample), |
||||
), |
||||
), |
||||
), |
||||
HomeScreenExampleItem( |
||||
title: 'Open a document by delta json', |
||||
icon: const Icon( |
||||
Icons.file_copy, |
||||
size: 50, |
||||
), |
||||
text: 'If you want to load a document by delta json file', |
||||
onPressed: () async { |
||||
final scaffoldMessenger = ScaffoldMessenger.of(context); |
||||
final navigator = Navigator.of(context); |
||||
try { |
||||
final result = await FilePicker.platform.pickFiles( |
||||
dialogTitle: 'Pick json delta', |
||||
type: FileType.custom, |
||||
allowedExtensions: ['json'], |
||||
allowMultiple: false, |
||||
); |
||||
final file = result?.files.firstOrNull; |
||||
final filePath = file?.path; |
||||
if (file == null || filePath == null) { |
||||
return; |
||||
} |
||||
final jsonString = await XFile(filePath).readAsString(); |
||||
|
||||
navigator.pushNamed( |
||||
QuillScreen.routeName, |
||||
arguments: QuillScreenArgs( |
||||
document: Document.fromJson(jsonDecode(jsonString)), |
||||
), |
||||
); |
||||
} catch (e) { |
||||
print( |
||||
'Error while loading json delta file: ${e.toString()}', |
||||
); |
||||
scaffoldMessenger.showText( |
||||
'Error while loading json delta file: ${e.toString()}', |
||||
); |
||||
} |
||||
}, |
||||
), |
||||
HomeScreenExampleItem( |
||||
title: 'Empty', |
||||
icon: const Icon( |
||||
Icons.insert_drive_file, |
||||
size: 50, |
||||
), |
||||
text: 'Want start clean? be my guest', |
||||
onPressed: () => Navigator.of(context).pushNamed( |
||||
QuillScreen.routeName, |
||||
arguments: QuillScreenArgs( |
||||
document: Document(), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,138 @@ |
||||
import 'dart:io' as io show Directory, File; |
||||
import 'dart:ui' show FontFeature; |
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart' |
||||
show CachedNetworkImageProvider; |
||||
import 'package:desktop_drop/desktop_drop.dart' show DropTarget; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/extensions.dart' show isAndroid, isIOS, isWeb; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; |
||||
import 'package:flutter_quill_extensions/presentation/embeds/widgets/image.dart' |
||||
show getImageProviderByImageSource, imageFileExtensions; |
||||
import 'package:path/path.dart' as path; |
||||
|
||||
import '../extensions/scaffold_messenger.dart'; |
||||
import 'embeds/timestamp_embed.dart'; |
||||
|
||||
class MyQuillEditor extends StatelessWidget { |
||||
const MyQuillEditor({ |
||||
required this.configurations, |
||||
required this.scrollController, |
||||
required this.focusNode, |
||||
super.key, |
||||
}); |
||||
|
||||
final QuillEditorConfigurations configurations; |
||||
final ScrollController scrollController; |
||||
final FocusNode focusNode; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return QuillEditor( |
||||
scrollController: scrollController, |
||||
focusNode: focusNode, |
||||
configurations: configurations.copyWith( |
||||
customStyles: const DefaultStyles( |
||||
h1: DefaultTextBlockStyle( |
||||
TextStyle( |
||||
fontSize: 32, |
||||
height: 1.15, |
||||
fontWeight: FontWeight.w300, |
||||
), |
||||
VerticalSpacing(16, 0), |
||||
VerticalSpacing(0, 0), |
||||
null, |
||||
), |
||||
sizeSmall: TextStyle(fontSize: 9), |
||||
subscript: TextStyle( |
||||
fontFamily: 'SF-UI-Display', |
||||
fontFeatures: [FontFeature.subscripts()], |
||||
), |
||||
superscript: TextStyle( |
||||
fontFamily: 'SF-UI-Display', |
||||
fontFeatures: [FontFeature.superscripts()], |
||||
), |
||||
), |
||||
scrollable: true, |
||||
placeholder: 'Start writting your notes...', |
||||
padding: const EdgeInsets.all(16), |
||||
onImagePaste: (imageBytes) async { |
||||
if (isWeb()) { |
||||
return null; |
||||
} |
||||
// We will save it to system temporary files |
||||
final newFileName = '${DateTime.now().toIso8601String()}.png'; |
||||
final newPath = path.join( |
||||
io.Directory.systemTemp.path, |
||||
newFileName, |
||||
); |
||||
final file = await io.File( |
||||
newPath, |
||||
).writeAsBytes(imageBytes, flush: true); |
||||
return file.path; |
||||
}, |
||||
embedBuilders: [ |
||||
...(isWeb() |
||||
? FlutterQuillEmbeds.editorWebBuilders() |
||||
: FlutterQuillEmbeds.editorBuilders( |
||||
imageEmbedConfigurations: QuillEditorImageEmbedConfigurations( |
||||
imageErrorWidgetBuilder: (context, error, stackTrace) { |
||||
return Text( |
||||
'Error while loading an image: ${error.toString()}', |
||||
); |
||||
}, |
||||
imageProviderBuilder: (imageUrl) { |
||||
// cached_network_image is supported |
||||
// only for Android, iOS and web |
||||
|
||||
// We will use it only if image from network |
||||
if (isAndroid(supportWeb: false) || |
||||
isIOS(supportWeb: false) || |
||||
isWeb()) { |
||||
if (isHttpBasedUrl(imageUrl)) { |
||||
return CachedNetworkImageProvider( |
||||
imageUrl, |
||||
); |
||||
} |
||||
} |
||||
return getImageProviderByImageSource( |
||||
imageUrl, |
||||
imageProviderBuilder: null, |
||||
assetsPrefix: QuillSharedExtensionsConfigurations.get( |
||||
context: context) |
||||
.assetsPrefix, |
||||
); |
||||
}, |
||||
), |
||||
)), |
||||
TimeStampEmbedBuilderWidget(), |
||||
], |
||||
builder: (context, rawEditor) { |
||||
// The `desktop_drop` plugin doesn't support iOS platform for now |
||||
if (isIOS(supportWeb: false)) { |
||||
return rawEditor; |
||||
} |
||||
return DropTarget( |
||||
onDragDone: (details) { |
||||
final scaffoldMessenger = ScaffoldMessenger.of(context); |
||||
final file = details.files.first; |
||||
final isSupported = imageFileExtensions.any(file.name.endsWith); |
||||
if (!isSupported) { |
||||
scaffoldMessenger.showText( |
||||
'Only images are supported right now: ${file.mimeType}, ${file.name}, ${file.path}, $imageFileExtensions', |
||||
); |
||||
return; |
||||
} |
||||
context.requireQuillController.insertImageBlock( |
||||
imageSource: file.path, |
||||
); |
||||
scaffoldMessenger.showText('Image is inserted.'); |
||||
}, |
||||
child: rawEditor, |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,144 @@ |
||||
import 'dart:convert' show jsonEncode; |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart' |
||||
show FlutterQuillEmbeds, QuillSharedExtensionsConfigurations; |
||||
|
||||
import 'package:quill_html_converter/quill_html_converter.dart'; |
||||
import 'package:share_plus/share_plus.dart' show Share; |
||||
|
||||
import '../extensions/scaffold_messenger.dart'; |
||||
import '../shared/widgets/home_screen_button.dart'; |
||||
import 'quill_editor.dart'; |
||||
import 'quill_toolbar.dart'; |
||||
|
||||
@immutable |
||||
class QuillScreenArgs { |
||||
const QuillScreenArgs({required this.document}); |
||||
|
||||
final Document document; |
||||
} |
||||
|
||||
class QuillScreen extends StatefulWidget { |
||||
const QuillScreen({ |
||||
required this.args, |
||||
super.key, |
||||
}); |
||||
|
||||
final QuillScreenArgs args; |
||||
|
||||
static const routeName = '/quill'; |
||||
|
||||
@override |
||||
State<QuillScreen> createState() => _QuillScreenState(); |
||||
} |
||||
|
||||
class _QuillScreenState extends State<QuillScreen> { |
||||
final _controller = QuillController.basic(); |
||||
final _editorFocusNode = FocusNode(); |
||||
final _editorScrollController = ScrollController(); |
||||
var _isReadOnly = false; |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_controller.document = widget.args.document; |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
_controller.dispose(); |
||||
_editorFocusNode.dispose(); |
||||
_editorScrollController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Scaffold( |
||||
appBar: AppBar( |
||||
title: const Text('Flutter Quill'), |
||||
actions: [ |
||||
IconButton( |
||||
tooltip: 'Load with HTML', |
||||
onPressed: () { |
||||
final html = _controller.document.toDelta().toHtml(); |
||||
_controller.document = |
||||
Document.fromDelta(DeltaHtmlExt.fromHtml(html)); |
||||
}, |
||||
icon: const Icon(Icons.html), |
||||
), |
||||
IconButton( |
||||
tooltip: 'Share', |
||||
onPressed: () { |
||||
final plainText = _controller.document.toPlainText( |
||||
FlutterQuillEmbeds.defaultEditorBuilders(), |
||||
); |
||||
if (plainText.trim().isEmpty) { |
||||
ScaffoldMessenger.of(context).showText( |
||||
"We can't share empty document, please enter some text first", |
||||
); |
||||
return; |
||||
} |
||||
Share.share(plainText); |
||||
}, |
||||
icon: const Icon(Icons.share), |
||||
), |
||||
IconButton( |
||||
tooltip: 'Print to log', |
||||
onPressed: () { |
||||
print( |
||||
jsonEncode(_controller.document.toDelta().toJson()), |
||||
); |
||||
ScaffoldMessenger.of(context).showText( |
||||
'The quill delta json has been printed to the log.', |
||||
); |
||||
}, |
||||
icon: const Icon(Icons.print), |
||||
), |
||||
const HomeScreenButton(), |
||||
], |
||||
), |
||||
body: QuillProvider( |
||||
configurations: QuillConfigurations( |
||||
controller: _controller, |
||||
sharedConfigurations: QuillSharedConfigurations( |
||||
animationConfigurations: QuillAnimationConfigurations.disableAll(), |
||||
extraConfigurations: const { |
||||
QuillSharedExtensionsConfigurations.key: |
||||
QuillSharedExtensionsConfigurations( |
||||
assetsPrefix: 'assets', |
||||
), |
||||
}, |
||||
), |
||||
), |
||||
child: Column( |
||||
children: [ |
||||
if (!_isReadOnly) |
||||
MyQuillToolbar( |
||||
focusNode: _editorFocusNode, |
||||
), |
||||
Builder( |
||||
builder: (context) { |
||||
return Expanded( |
||||
child: MyQuillEditor( |
||||
configurations: QuillEditorConfigurations( |
||||
readOnly: _isReadOnly, |
||||
), |
||||
scrollController: _editorScrollController, |
||||
focusNode: _editorFocusNode, |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
], |
||||
), |
||||
), |
||||
floatingActionButton: FloatingActionButton( |
||||
child: Icon(_isReadOnly ? Icons.lock : Icons.edit), |
||||
onPressed: () => setState(() => _isReadOnly = !_isReadOnly), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,301 @@ |
||||
import 'dart:io' as io show File; |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_bloc/flutter_bloc.dart'; |
||||
import 'package:flutter_quill/extensions.dart' show isAndroid, isIOS, isWeb; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; |
||||
import 'package:image_cropper/image_cropper.dart'; |
||||
import 'package:path/path.dart' as path; |
||||
import 'package:path_provider/path_provider.dart' |
||||
show getApplicationDocumentsDirectory; |
||||
|
||||
import '../extensions/scaffold_messenger.dart'; |
||||
import '../settings/cubit/settings_cubit.dart'; |
||||
import 'embeds/timestamp_embed.dart'; |
||||
|
||||
class MyQuillToolbar extends StatelessWidget { |
||||
const MyQuillToolbar({ |
||||
required this.focusNode, |
||||
super.key, |
||||
}); |
||||
|
||||
final FocusNode focusNode; |
||||
|
||||
Future<void> onImageInsertWithCropping( |
||||
String image, |
||||
QuillController controller, |
||||
BuildContext context, |
||||
) async { |
||||
final croppedFile = await ImageCropper().cropImage( |
||||
sourcePath: image, |
||||
aspectRatioPresets: [ |
||||
CropAspectRatioPreset.square, |
||||
CropAspectRatioPreset.ratio3x2, |
||||
CropAspectRatioPreset.original, |
||||
CropAspectRatioPreset.ratio4x3, |
||||
CropAspectRatioPreset.ratio16x9 |
||||
], |
||||
uiSettings: [ |
||||
AndroidUiSettings( |
||||
toolbarTitle: 'Cropper', |
||||
toolbarColor: Colors.deepOrange, |
||||
toolbarWidgetColor: Colors.white, |
||||
initAspectRatio: CropAspectRatioPreset.original, |
||||
lockAspectRatio: false, |
||||
), |
||||
IOSUiSettings( |
||||
title: 'Cropper', |
||||
), |
||||
WebUiSettings( |
||||
context: context, |
||||
), |
||||
], |
||||
); |
||||
final newImage = croppedFile?.path; |
||||
if (newImage == null) { |
||||
return; |
||||
} |
||||
if (isWeb()) { |
||||
controller.insertImageBlock(imageSource: newImage); |
||||
return; |
||||
} |
||||
final newSavedImage = await saveImage(io.File(newImage)); |
||||
controller.insertImageBlock(imageSource: newSavedImage); |
||||
} |
||||
|
||||
Future<void> onImageInsert(String image, QuillController controller) async { |
||||
if (isWeb()) { |
||||
controller.insertImageBlock(imageSource: image); |
||||
return; |
||||
} |
||||
final newSavedImage = await saveImage(io.File(image)); |
||||
controller.insertImageBlock(imageSource: newSavedImage); |
||||
} |
||||
|
||||
/// For mobile platforms it will copies the picked file from temporary cache |
||||
/// to applications directory |
||||
/// |
||||
/// for desktop platforms, it will do the same but from user files this time |
||||
Future<String> saveImage(io.File file) async { |
||||
final appDocDir = await getApplicationDocumentsDirectory(); |
||||
final fileExt = path.extension(file.path); |
||||
final newFileName = '${DateTime.now().toIso8601String()}$fileExt'; |
||||
final newPath = path.join( |
||||
appDocDir.path, |
||||
newFileName, |
||||
); |
||||
final copiedFile = await file.copy(newPath); |
||||
return copiedFile.path; |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return BlocBuilder<SettingsCubit, SettingsState>( |
||||
buildWhen: (previous, current) => |
||||
previous.useCustomQuillToolbar != current.useCustomQuillToolbar, |
||||
builder: (context, state) { |
||||
if (state.useCustomQuillToolbar) { |
||||
// For more info |
||||
// https://github.com/singerdmx/flutter-quill/blob/master/doc/custom_toolbar.md |
||||
return QuillToolbarProvider( |
||||
toolbarConfigurations: const QuillToolbarConfigurations(), |
||||
child: QuillBaseToolbar( |
||||
configurations: QuillBaseToolbarConfigurations( |
||||
toolbarSize: 15 * 2, |
||||
multiRowsDisplay: false, |
||||
childrenBuilder: (context) { |
||||
final controller = context.requireQuillController; |
||||
return [ |
||||
QuillToolbarImageButton( |
||||
controller: controller, |
||||
options: const QuillToolbarImageButtonOptions(), |
||||
), |
||||
QuillToolbarHistoryButton( |
||||
controller: controller, |
||||
options: |
||||
const QuillToolbarHistoryButtonOptions(isUndo: true), |
||||
), |
||||
QuillToolbarHistoryButton( |
||||
controller: controller, |
||||
options: |
||||
const QuillToolbarHistoryButtonOptions(isUndo: false), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.bold, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_bold, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.italic, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_italic, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.underline, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_underline, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarClearFormatButton( |
||||
controller: controller, |
||||
options: const QuillToolbarClearFormatButtonOptions( |
||||
iconData: Icons.format_clear, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
VerticalDivider( |
||||
indent: 12, |
||||
endIndent: 12, |
||||
color: Colors.grey.shade400, |
||||
), |
||||
QuillToolbarSelectHeaderStyleButtons( |
||||
controller: controller, |
||||
options: |
||||
const QuillToolbarSelectHeaderStyleButtonsOptions( |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.ol, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_list_numbered, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.ul, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_list_bulleted, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
QuillToolbarToggleStyleButton( |
||||
attribute: Attribute.blockQuote, |
||||
controller: controller, |
||||
options: const QuillToolbarToggleStyleButtonOptions( |
||||
iconData: Icons.format_quote, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
VerticalDivider( |
||||
indent: 12, |
||||
endIndent: 12, |
||||
color: Colors.grey.shade400, |
||||
), |
||||
QuillToolbarIndentButton( |
||||
controller: controller, |
||||
isIncrease: true, |
||||
options: const QuillToolbarIndentButtonOptions( |
||||
iconData: Icons.format_indent_increase, |
||||
iconSize: 20, |
||||
)), |
||||
QuillToolbarIndentButton( |
||||
controller: controller, |
||||
isIncrease: false, |
||||
options: const QuillToolbarIndentButtonOptions( |
||||
iconData: Icons.format_indent_decrease, |
||||
iconSize: 20, |
||||
), |
||||
), |
||||
]; |
||||
}, |
||||
), |
||||
), |
||||
); |
||||
} |
||||
return QuillToolbar( |
||||
configurations: QuillToolbarConfigurations( |
||||
showAlignmentButtons: true, |
||||
buttonOptions: QuillToolbarButtonOptions( |
||||
base: QuillToolbarBaseButtonOptions( |
||||
// Request editor focus when any button is pressed |
||||
afterButtonPressed: focusNode.requestFocus, |
||||
), |
||||
), |
||||
customButtons: [ |
||||
QuillToolbarCustomButtonOptions( |
||||
icon: const Icon(Icons.add_alarm_rounded), |
||||
onPressed: () { |
||||
final controller = context.requireQuillController; |
||||
controller.document |
||||
.insert(controller.selection.extentOffset, '\n'); |
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.local, |
||||
); |
||||
|
||||
controller.document.insert( |
||||
controller.selection.extentOffset, |
||||
TimeStampEmbed( |
||||
DateTime.now().toString(), |
||||
), |
||||
); |
||||
|
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.local, |
||||
); |
||||
|
||||
controller.document |
||||
.insert(controller.selection.extentOffset, ' '); |
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.local, |
||||
); |
||||
|
||||
controller.document |
||||
.insert(controller.selection.extentOffset, '\n'); |
||||
controller.updateSelection( |
||||
TextSelection.collapsed( |
||||
offset: controller.selection.extentOffset + 1, |
||||
), |
||||
ChangeSource.local, |
||||
); |
||||
}, |
||||
), |
||||
QuillToolbarCustomButtonOptions( |
||||
icon: const Icon(Icons.ac_unit), |
||||
onPressed: () { |
||||
ScaffoldMessenger.of(context) |
||||
..clearSnackBars() |
||||
..showText( |
||||
'Custom button!', |
||||
); |
||||
}, |
||||
), |
||||
], |
||||
embedButtons: FlutterQuillEmbeds.toolbarButtons( |
||||
imageButtonOptions: QuillToolbarImageButtonOptions( |
||||
imageButtonConfigurations: QuillToolbarImageConfigurations( |
||||
onImageInsertCallback: isAndroid(supportWeb: false) || |
||||
isIOS(supportWeb: false) || |
||||
isWeb() |
||||
? (image, controller) => |
||||
onImageInsertWithCropping(image, controller, context) |
||||
: onImageInsert, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,295 @@ |
||||
import '../../../gen/assets.gen.dart'; |
||||
|
||||
final quillDefaultSample = [ |
||||
{ |
||||
'insert': {'image': Assets.images.screenshot1.path}, |
||||
'attributes': { |
||||
'width': '100', |
||||
'height': '100', |
||||
'style': 'width:500px; height:350px;' |
||||
} |
||||
}, |
||||
{'insert': 'Flutter Quill'}, |
||||
{ |
||||
'attributes': {'header': 1}, |
||||
'insert': '\n' |
||||
}, |
||||
{ |
||||
'insert': { |
||||
'video': |
||||
'https://www.youtube.com/watch?v=V4hgdKhIqtc&list=PLbhaS_83B97s78HsDTtplRTEhcFsqSqIK&index=1' |
||||
} |
||||
}, |
||||
{ |
||||
'insert': { |
||||
'video': |
||||
'https://user-images.githubusercontent.com/122956/126238875-22e42501-ad41-4266-b1d6-3f89b5e3b79b.mp4' |
||||
} |
||||
}, |
||||
{'insert': '\nRich text editor for Flutter'}, |
||||
{ |
||||
'attributes': {'header': 2}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Quill component for Flutter'}, |
||||
{ |
||||
'attributes': {'header': 3}, |
||||
'insert': '\n' |
||||
}, |
||||
{ |
||||
'attributes': {'link': 'https://bulletjournal.us/home/index.html'}, |
||||
'insert': 'Bullet Journal' |
||||
}, |
||||
{ |
||||
'insert': |
||||
':\nTrack personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders' |
||||
}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{ |
||||
'insert': |
||||
'Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices' |
||||
}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Check out what you and your teammates are working on each day'}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\nSplitting bills with friends can never be easier.'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Start creating a group and invite your friends to join.'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Create a BuJo of Ledger type to see expense or balance summary.'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{ |
||||
'insert': |
||||
'\nAttach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s).' |
||||
}, |
||||
{ |
||||
'attributes': {'blockquote': true}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': "\nvar BuJo = 'Bullet' + 'Journal'"}, |
||||
{ |
||||
'attributes': {'code-block': true}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\nStart tracking in your browser'}, |
||||
{ |
||||
'attributes': {'indent': 1}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Stop the timer on your phone'}, |
||||
{ |
||||
'attributes': {'indent': 1}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'All your time entries are synced'}, |
||||
{ |
||||
'attributes': {'indent': 2}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'between the phone apps'}, |
||||
{ |
||||
'attributes': {'indent': 2}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'and the website.'}, |
||||
{ |
||||
'attributes': {'indent': 3}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\n'}, |
||||
{'insert': '\nCenter Align'}, |
||||
{ |
||||
'attributes': {'align': 'center'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Right Align'}, |
||||
{ |
||||
'attributes': {'align': 'right'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Justify Align'}, |
||||
{ |
||||
'attributes': {'align': 'justify'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Have trouble finding things? '}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Just type in the search bar'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'and easily find contents'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'across projects or folders.'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'It matches text in your note or task.'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Enable reminders so that you will get notified by'}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'email'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'message on your phone'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'popup on the web site'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Create a BuJo serving as project or folder'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Organize your'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'tasks'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'notes'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'transactions'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'under BuJo '}, |
||||
{ |
||||
'attributes': {'indent': 3, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'See them in Calendar'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'or hierarchical view'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'this is a check list'}, |
||||
{ |
||||
'attributes': {'list': 'checked'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'this is a uncheck list'}, |
||||
{ |
||||
'attributes': {'list': 'unchecked'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Font '}, |
||||
{ |
||||
'attributes': {'font': 'sans-serif'}, |
||||
'insert': 'Sans Serif' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'font': 'serif'}, |
||||
'insert': 'Serif' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'font': 'monospace'}, |
||||
'insert': 'Monospace' |
||||
}, |
||||
{'insert': ' Size '}, |
||||
{ |
||||
'attributes': {'size': 'small'}, |
||||
'insert': 'Small' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': 'large'}, |
||||
'insert': 'Large' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': 'huge'}, |
||||
'insert': 'Huge' |
||||
}, |
||||
{ |
||||
'attributes': {'size': '15.0'}, |
||||
'insert': 'font size 15' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': '35'}, |
||||
'insert': 'font size 35' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': '20'}, |
||||
'insert': 'font size 20' |
||||
}, |
||||
{ |
||||
'attributes': {'token': 'built_in'}, |
||||
'insert': ' diff' |
||||
}, |
||||
{ |
||||
'attributes': {'token': 'operator'}, |
||||
'insert': '-match' |
||||
}, |
||||
{ |
||||
'attributes': {'token': 'literal'}, |
||||
'insert': '-patch' |
||||
}, |
||||
{ |
||||
'insert': { |
||||
'image': |
||||
'https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png' |
||||
}, |
||||
'attributes': { |
||||
'width': '230', |
||||
'style': 'display: block; margin: auto; width: 500px;' |
||||
} |
||||
}, |
||||
{'insert': '\n'} |
||||
]; |
@ -0,0 +1,270 @@ |
||||
const quillTextSample = [ |
||||
{'insert': 'Flutter Quill'}, |
||||
{ |
||||
'attributes': {'header': 1}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\nRich text editor for Flutter'}, |
||||
{ |
||||
'attributes': {'header': 2}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Quill component for Flutter'}, |
||||
{ |
||||
'attributes': {'color': 'rgba(0, 0, 0, 0.847)'}, |
||||
'insert': ' and ' |
||||
}, |
||||
{ |
||||
'attributes': {'link': 'https://bulletjournal.us/home/index.html'}, |
||||
'insert': 'Bullet Journal' |
||||
}, |
||||
{ |
||||
'insert': |
||||
':\nTrack personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders' |
||||
}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{ |
||||
'insert': |
||||
'Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices' |
||||
}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Check out what you and your teammates are working on each day'}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\nSplitting bills with friends can never be easier.'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Start creating a group and invite your friends to join.'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Create a BuJo of Ledger type to see expense or balance summary.'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{ |
||||
'insert': |
||||
'\nAttach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s).' |
||||
}, |
||||
{ |
||||
'attributes': {'blockquote': true}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': "\nvar BuJo = 'Bullet' + 'Journal'"}, |
||||
{ |
||||
'attributes': {'code-block': true}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\nStart tracking in your browser'}, |
||||
{ |
||||
'attributes': {'indent': 1}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Stop the timer on your phone'}, |
||||
{ |
||||
'attributes': {'indent': 1}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'All your time entries are synced'}, |
||||
{ |
||||
'attributes': {'indent': 2}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'between the phone apps'}, |
||||
{ |
||||
'attributes': {'indent': 2}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'and the website.'}, |
||||
{ |
||||
'attributes': {'indent': 3}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': '\n'}, |
||||
{'insert': '\nCenter Align'}, |
||||
{ |
||||
'attributes': {'align': 'center'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Right Align'}, |
||||
{ |
||||
'attributes': {'align': 'right'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Justify Align'}, |
||||
{ |
||||
'attributes': {'align': 'justify'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Have trouble finding things? '}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Just type in the search bar'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'and easily find contents'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'across projects or folders.'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'It matches text in your note or task.'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Enable reminders so that you will get notified by'}, |
||||
{ |
||||
'attributes': {'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'email'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'message on your phone'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'popup on the web site'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'ordered'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Create a BuJo serving as project or folder'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Organize your'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'tasks'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'notes'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'transactions'}, |
||||
{ |
||||
'attributes': {'indent': 2, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'under BuJo '}, |
||||
{ |
||||
'attributes': {'indent': 3, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'See them in Calendar'}, |
||||
{ |
||||
'attributes': {'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'or hierarchical view'}, |
||||
{ |
||||
'attributes': {'indent': 1, 'list': 'bullet'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'this is a check list'}, |
||||
{ |
||||
'attributes': {'list': 'checked'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'this is a uncheck list'}, |
||||
{ |
||||
'attributes': {'list': 'unchecked'}, |
||||
'insert': '\n' |
||||
}, |
||||
{'insert': 'Font '}, |
||||
{ |
||||
'attributes': {'font': 'sans-serif'}, |
||||
'insert': 'Sans Serif' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'font': 'serif'}, |
||||
'insert': 'Serif' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'font': 'monospace'}, |
||||
'insert': 'Monospace' |
||||
}, |
||||
{'insert': ' Size '}, |
||||
{ |
||||
'attributes': {'size': 'small'}, |
||||
'insert': 'Small' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': 'large'}, |
||||
'insert': 'Large' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': 'huge'}, |
||||
'insert': 'Huge' |
||||
}, |
||||
{ |
||||
'attributes': {'size': '15.0'}, |
||||
'insert': 'font size 15' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': '35'}, |
||||
'insert': 'font size 35' |
||||
}, |
||||
{'insert': ' '}, |
||||
{ |
||||
'attributes': {'size': '20'}, |
||||
'insert': 'font size 20' |
||||
}, |
||||
{ |
||||
'attributes': {'token': 'built_in'}, |
||||
'insert': ' diff' |
||||
}, |
||||
{ |
||||
'attributes': {'token': 'operator'}, |
||||
'insert': '-match' |
||||
}, |
||||
{ |
||||
'attributes': {'token': 'literal'}, |
||||
'insert': '-patch' |
||||
}, |
||||
{ |
||||
'insert': { |
||||
'image': |
||||
'https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png' |
||||
}, |
||||
'attributes': {'width': '230', 'style': 'display: block; margin: auto;'} |
||||
}, |
||||
{'insert': '\n'} |
||||
]; |
@ -0,0 +1,19 @@ |
||||
const quillVideosSample = [ |
||||
{'insert': '\n'}, |
||||
{ |
||||
'insert': {'video': 'https://youtu.be/xz6_AlJkDPA'}, |
||||
'attributes': { |
||||
'width': '300', |
||||
'height': '300', |
||||
'style': 'width:400px; height:500px;' |
||||
} |
||||
}, |
||||
{'insert': '\n'}, |
||||
{'insert': '\n'}, |
||||
{'insert': 'And this is just a youtube video'}, |
||||
{'insert': '\n'}, |
||||
{ |
||||
'insert': 'This sample is not complete.', |
||||
}, |
||||
{'insert': '\n'}, |
||||
]; |
@ -0,0 +1,26 @@ |
||||
import 'package:bloc/bloc.dart'; |
||||
import 'package:flutter/material.dart' show ThemeMode; |
||||
import 'package:freezed_annotation/freezed_annotation.dart'; |
||||
import 'package:hydrated_bloc/hydrated_bloc.dart' show HydratedMixin; |
||||
|
||||
part 'settings_state.dart'; |
||||
part 'settings_cubit.freezed.dart'; |
||||
part 'settings_cubit.g.dart'; |
||||
|
||||
class SettingsCubit extends Cubit<SettingsState> with HydratedMixin { |
||||
SettingsCubit() : super(const SettingsState()); |
||||
|
||||
void updateSettings(SettingsState newSettingsState) { |
||||
emit(newSettingsState); |
||||
} |
||||
|
||||
@override |
||||
SettingsState? fromJson(Map<String, dynamic> json) { |
||||
return SettingsState.fromJson(json); |
||||
} |
||||
|
||||
@override |
||||
Map<String, dynamic>? toJson(SettingsState state) { |
||||
return state.toJson(); |
||||
} |
||||
} |
@ -0,0 +1,202 @@ |
||||
// coverage:ignore-file |
||||
// GENERATED CODE - DO NOT MODIFY BY HAND |
||||
// ignore_for_file: type=lint |
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark |
||||
|
||||
part of 'settings_cubit.dart'; |
||||
|
||||
// ************************************************************************** |
||||
// FreezedGenerator |
||||
// ************************************************************************** |
||||
|
||||
T _$identity<T>(T value) => value; |
||||
|
||||
final _privateConstructorUsedError = UnsupportedError( |
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); |
||||
|
||||
SettingsState _$SettingsStateFromJson(Map<String, dynamic> json) { |
||||
return _SettingsState.fromJson(json); |
||||
} |
||||
|
||||
/// @nodoc |
||||
mixin _$SettingsState { |
||||
ThemeMode get themeMode => throw _privateConstructorUsedError; |
||||
DefaultScreen get defaultScreen => throw _privateConstructorUsedError; |
||||
bool get useCustomQuillToolbar => throw _privateConstructorUsedError; |
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; |
||||
@JsonKey(ignore: true) |
||||
$SettingsStateCopyWith<SettingsState> get copyWith => |
||||
throw _privateConstructorUsedError; |
||||
} |
||||
|
||||
/// @nodoc |
||||
abstract class $SettingsStateCopyWith<$Res> { |
||||
factory $SettingsStateCopyWith( |
||||
SettingsState value, $Res Function(SettingsState) then) = |
||||
_$SettingsStateCopyWithImpl<$Res, SettingsState>; |
||||
@useResult |
||||
$Res call( |
||||
{ThemeMode themeMode, |
||||
DefaultScreen defaultScreen, |
||||
bool useCustomQuillToolbar}); |
||||
} |
||||
|
||||
/// @nodoc |
||||
class _$SettingsStateCopyWithImpl<$Res, $Val extends SettingsState> |
||||
implements $SettingsStateCopyWith<$Res> { |
||||
_$SettingsStateCopyWithImpl(this._value, this._then); |
||||
|
||||
// ignore: unused_field |
||||
final $Val _value; |
||||
// ignore: unused_field |
||||
final $Res Function($Val) _then; |
||||
|
||||
@pragma('vm:prefer-inline') |
||||
@override |
||||
$Res call({ |
||||
Object? themeMode = null, |
||||
Object? defaultScreen = null, |
||||
Object? useCustomQuillToolbar = null, |
||||
}) { |
||||
return _then(_value.copyWith( |
||||
themeMode: null == themeMode |
||||
? _value.themeMode |
||||
: themeMode // ignore: cast_nullable_to_non_nullable |
||||
as ThemeMode, |
||||
defaultScreen: null == defaultScreen |
||||
? _value.defaultScreen |
||||
: defaultScreen // ignore: cast_nullable_to_non_nullable |
||||
as DefaultScreen, |
||||
useCustomQuillToolbar: null == useCustomQuillToolbar |
||||
? _value.useCustomQuillToolbar |
||||
: useCustomQuillToolbar // ignore: cast_nullable_to_non_nullable |
||||
as bool, |
||||
) as $Val); |
||||
} |
||||
} |
||||
|
||||
/// @nodoc |
||||
abstract class _$$SettingsStateImplCopyWith<$Res> |
||||
implements $SettingsStateCopyWith<$Res> { |
||||
factory _$$SettingsStateImplCopyWith( |
||||
_$SettingsStateImpl value, $Res Function(_$SettingsStateImpl) then) = |
||||
__$$SettingsStateImplCopyWithImpl<$Res>; |
||||
@override |
||||
@useResult |
||||
$Res call( |
||||
{ThemeMode themeMode, |
||||
DefaultScreen defaultScreen, |
||||
bool useCustomQuillToolbar}); |
||||
} |
||||
|
||||
/// @nodoc |
||||
class __$$SettingsStateImplCopyWithImpl<$Res> |
||||
extends _$SettingsStateCopyWithImpl<$Res, _$SettingsStateImpl> |
||||
implements _$$SettingsStateImplCopyWith<$Res> { |
||||
__$$SettingsStateImplCopyWithImpl( |
||||
_$SettingsStateImpl _value, $Res Function(_$SettingsStateImpl) _then) |
||||
: super(_value, _then); |
||||
|
||||
@pragma('vm:prefer-inline') |
||||
@override |
||||
$Res call({ |
||||
Object? themeMode = null, |
||||
Object? defaultScreen = null, |
||||
Object? useCustomQuillToolbar = null, |
||||
}) { |
||||
return _then(_$SettingsStateImpl( |
||||
themeMode: null == themeMode |
||||
? _value.themeMode |
||||
: themeMode // ignore: cast_nullable_to_non_nullable |
||||
as ThemeMode, |
||||
defaultScreen: null == defaultScreen |
||||
? _value.defaultScreen |
||||
: defaultScreen // ignore: cast_nullable_to_non_nullable |
||||
as DefaultScreen, |
||||
useCustomQuillToolbar: null == useCustomQuillToolbar |
||||
? _value.useCustomQuillToolbar |
||||
: useCustomQuillToolbar // ignore: cast_nullable_to_non_nullable |
||||
as bool, |
||||
)); |
||||
} |
||||
} |
||||
|
||||
/// @nodoc |
||||
@JsonSerializable() |
||||
class _$SettingsStateImpl implements _SettingsState { |
||||
const _$SettingsStateImpl( |
||||
{this.themeMode = ThemeMode.system, |
||||
this.defaultScreen = DefaultScreen.home, |
||||
this.useCustomQuillToolbar = false}); |
||||
|
||||
factory _$SettingsStateImpl.fromJson(Map<String, dynamic> json) => |
||||
_$$SettingsStateImplFromJson(json); |
||||
|
||||
@override |
||||
@JsonKey() |
||||
final ThemeMode themeMode; |
||||
@override |
||||
@JsonKey() |
||||
final DefaultScreen defaultScreen; |
||||
@override |
||||
@JsonKey() |
||||
final bool useCustomQuillToolbar; |
||||
|
||||
@override |
||||
String toString() { |
||||
return 'SettingsState(themeMode: $themeMode, defaultScreen: $defaultScreen, useCustomQuillToolbar: $useCustomQuillToolbar)'; |
||||
} |
||||
|
||||
@override |
||||
bool operator ==(dynamic other) { |
||||
return identical(this, other) || |
||||
(other.runtimeType == runtimeType && |
||||
other is _$SettingsStateImpl && |
||||
(identical(other.themeMode, themeMode) || |
||||
other.themeMode == themeMode) && |
||||
(identical(other.defaultScreen, defaultScreen) || |
||||
other.defaultScreen == defaultScreen) && |
||||
(identical(other.useCustomQuillToolbar, useCustomQuillToolbar) || |
||||
other.useCustomQuillToolbar == useCustomQuillToolbar)); |
||||
} |
||||
|
||||
@JsonKey(ignore: true) |
||||
@override |
||||
int get hashCode => |
||||
Object.hash(runtimeType, themeMode, defaultScreen, useCustomQuillToolbar); |
||||
|
||||
@JsonKey(ignore: true) |
||||
@override |
||||
@pragma('vm:prefer-inline') |
||||
_$$SettingsStateImplCopyWith<_$SettingsStateImpl> get copyWith => |
||||
__$$SettingsStateImplCopyWithImpl<_$SettingsStateImpl>(this, _$identity); |
||||
|
||||
@override |
||||
Map<String, dynamic> toJson() { |
||||
return _$$SettingsStateImplToJson( |
||||
this, |
||||
); |
||||
} |
||||
} |
||||
|
||||
abstract class _SettingsState implements SettingsState { |
||||
const factory _SettingsState( |
||||
{final ThemeMode themeMode, |
||||
final DefaultScreen defaultScreen, |
||||
final bool useCustomQuillToolbar}) = _$SettingsStateImpl; |
||||
|
||||
factory _SettingsState.fromJson(Map<String, dynamic> json) = |
||||
_$SettingsStateImpl.fromJson; |
||||
|
||||
@override |
||||
ThemeMode get themeMode; |
||||
@override |
||||
DefaultScreen get defaultScreen; |
||||
@override |
||||
bool get useCustomQuillToolbar; |
||||
@override |
||||
@JsonKey(ignore: true) |
||||
_$$SettingsStateImplCopyWith<_$SettingsStateImpl> get copyWith => |
||||
throw _privateConstructorUsedError; |
||||
} |
@ -0,0 +1,40 @@ |
||||
// GENERATED CODE - DO NOT MODIFY BY HAND |
||||
|
||||
part of 'settings_cubit.dart'; |
||||
|
||||
// ************************************************************************** |
||||
// JsonSerializableGenerator |
||||
// ************************************************************************** |
||||
|
||||
_$SettingsStateImpl _$$SettingsStateImplFromJson(Map<String, dynamic> json) => |
||||
_$SettingsStateImpl( |
||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? |
||||
ThemeMode.system, |
||||
defaultScreen: |
||||
$enumDecodeNullable(_$DefaultScreenEnumMap, json['defaultScreen']) ?? |
||||
DefaultScreen.home, |
||||
useCustomQuillToolbar: json['useCustomQuillToolbar'] as bool? ?? false, |
||||
); |
||||
|
||||
Map<String, dynamic> _$$SettingsStateImplToJson(_$SettingsStateImpl instance) => |
||||
<String, dynamic>{ |
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, |
||||
'defaultScreen': _$DefaultScreenEnumMap[instance.defaultScreen]!, |
||||
'useCustomQuillToolbar': instance.useCustomQuillToolbar, |
||||
}; |
||||
|
||||
const _$ThemeModeEnumMap = { |
||||
ThemeMode.system: 'system', |
||||
ThemeMode.light: 'light', |
||||
ThemeMode.dark: 'dark', |
||||
}; |
||||
|
||||
const _$DefaultScreenEnumMap = { |
||||
DefaultScreen.home: 'home', |
||||
DefaultScreen.settings: 'settings', |
||||
DefaultScreen.defaultSample: 'defaultSample', |
||||
DefaultScreen.imagesSample: 'imagesSample', |
||||
DefaultScreen.videosSample: 'videosSample', |
||||
DefaultScreen.textSample: 'textSample', |
||||
DefaultScreen.emptySample: 'emptySample', |
||||
}; |
@ -0,0 +1,22 @@ |
||||
part of 'settings_cubit.dart'; |
||||
|
||||
enum DefaultScreen { |
||||
home, |
||||
settings, |
||||
defaultSample, |
||||
imagesSample, |
||||
videosSample, |
||||
textSample, |
||||
emptySample, |
||||
} |
||||
|
||||
@freezed |
||||
class SettingsState with _$SettingsState { |
||||
const factory SettingsState({ |
||||
@Default(ThemeMode.system) ThemeMode themeMode, |
||||
@Default(DefaultScreen.home) DefaultScreen defaultScreen, |
||||
@Default(false) bool useCustomQuillToolbar, |
||||
}) = _SettingsState; |
||||
factory SettingsState.fromJson(Map<String, Object?> json) => |
||||
_$SettingsStateFromJson(json); |
||||
} |
@ -0,0 +1,122 @@ |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_bloc/flutter_bloc.dart'; |
||||
|
||||
import '../../shared/widgets/dialog_action.dart'; |
||||
import '../../shared/widgets/home_screen_button.dart'; |
||||
import '../cubit/settings_cubit.dart'; |
||||
|
||||
class SettingsScreen extends StatelessWidget { |
||||
const SettingsScreen({super.key}); |
||||
|
||||
static const routeName = '/settings'; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final materialTheme = Theme.of(context); |
||||
final isDark = materialTheme.brightness == Brightness.dark; |
||||
return Scaffold( |
||||
appBar: AppBar( |
||||
title: const Text('Settings'), |
||||
actions: const [ |
||||
HomeScreenButton(), |
||||
], |
||||
), |
||||
body: BlocBuilder<SettingsCubit, SettingsState>( |
||||
builder: (context, state) { |
||||
return ListView( |
||||
children: [ |
||||
CheckboxListTile.adaptive( |
||||
value: isDark, |
||||
onChanged: (value) { |
||||
final isNewValueDark = value ?? false; |
||||
context.read<SettingsCubit>().updateSettings( |
||||
state.copyWith( |
||||
themeMode: |
||||
isNewValueDark ? ThemeMode.dark : ThemeMode.light, |
||||
), |
||||
); |
||||
}, |
||||
title: const Text('Dark Theme'), |
||||
subtitle: const Text( |
||||
'By default we will use your system theme, but you can set if you want dark or light theme', |
||||
), |
||||
secondary: Icon(isDark ? Icons.nightlight : Icons.sunny), |
||||
), |
||||
ListTile( |
||||
title: const Text('Default screen'), |
||||
subtitle: const Text( |
||||
'Which screen should be used when the flutter app starts?', |
||||
), |
||||
leading: const Icon(Icons.home), |
||||
onTap: () async { |
||||
final settingsBloc = context.read<SettingsCubit>(); |
||||
final newDefaultScreen = |
||||
await showAdaptiveDialog<DefaultScreen>( |
||||
context: context, |
||||
builder: (context) { |
||||
return AlertDialog.adaptive( |
||||
title: const Text('Select default screen'), |
||||
content: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
...DefaultScreen.values.map( |
||||
(e) => Material( |
||||
child: ListTile( |
||||
onTap: () { |
||||
Navigator.of(context).pop(e); |
||||
}, |
||||
title: Text(e.name), |
||||
leading: CircleAvatar( |
||||
child: Text((e.index + 1).toString()), |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
actions: [ |
||||
AppDialogAction( |
||||
onPressed: () => Navigator.of(context).pop(null), |
||||
options: const DialogActionOptions( |
||||
cupertinoDialogActionOptions: |
||||
CupertinoDialogActionOptions( |
||||
isDefaultAction: true, |
||||
), |
||||
), |
||||
child: const Text('Cancel'), |
||||
), |
||||
], |
||||
); |
||||
}, |
||||
); |
||||
if (newDefaultScreen != null) { |
||||
settingsBloc.updateSettings( |
||||
settingsBloc.state |
||||
.copyWith(defaultScreen: newDefaultScreen), |
||||
); |
||||
} |
||||
}, |
||||
), |
||||
CheckboxListTile.adaptive( |
||||
value: state.useCustomQuillToolbar, |
||||
onChanged: (value) { |
||||
final useCustomToolbarNewValue = value ?? false; |
||||
context.read<SettingsCubit>().updateSettings( |
||||
state.copyWith( |
||||
useCustomQuillToolbar: useCustomToolbarNewValue, |
||||
), |
||||
); |
||||
}, |
||||
title: const Text('Use custom Quill toolbar'), |
||||
subtitle: const Text( |
||||
'By default we will default QuillToolbar, but you can decide if you the built-in or the custom one', |
||||
), |
||||
secondary: const Icon(Icons.dashboard_customize), |
||||
), |
||||
], |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,63 @@ |
||||
import 'package:flutter/cupertino.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/extensions.dart'; |
||||
|
||||
@immutable |
||||
final class CupertinoDialogActionOptions { |
||||
const CupertinoDialogActionOptions({ |
||||
this.isDefaultAction = false, |
||||
}); |
||||
|
||||
final bool isDefaultAction; |
||||
} |
||||
|
||||
@immutable |
||||
final class MaterialDialogActionOptions { |
||||
const MaterialDialogActionOptions({ |
||||
this.textStyle, |
||||
}); |
||||
|
||||
final ButtonStyle? textStyle; |
||||
} |
||||
|
||||
@immutable |
||||
class DialogActionOptions { |
||||
const DialogActionOptions({ |
||||
this.cupertinoDialogActionOptions, |
||||
this.materialDialogActionOptions, |
||||
}); |
||||
|
||||
final CupertinoDialogActionOptions? cupertinoDialogActionOptions; |
||||
final MaterialDialogActionOptions? materialDialogActionOptions; |
||||
} |
||||
|
||||
class AppDialogAction extends StatelessWidget { |
||||
const AppDialogAction({ |
||||
required this.child, |
||||
required this.onPressed, |
||||
this.options, |
||||
super.key, |
||||
}); |
||||
|
||||
final VoidCallback? onPressed; |
||||
final Widget child; |
||||
|
||||
final DialogActionOptions? options; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
if (isAppleOS(supportWeb: true)) { |
||||
return CupertinoDialogAction( |
||||
onPressed: onPressed, |
||||
isDefaultAction: |
||||
options?.cupertinoDialogActionOptions?.isDefaultAction ?? false, |
||||
child: child, |
||||
); |
||||
} |
||||
return TextButton( |
||||
onPressed: onPressed, |
||||
style: options?.materialDialogActionOptions?.textStyle, |
||||
child: child, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_bloc/flutter_bloc.dart'; |
||||
|
||||
import '../../settings/cubit/settings_cubit.dart'; |
||||
|
||||
class HomeScreenButton extends StatelessWidget { |
||||
const HomeScreenButton({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return IconButton( |
||||
onPressed: () { |
||||
final settingsCubit = context.read<SettingsCubit>(); |
||||
settingsCubit.updateSettings( |
||||
settingsCubit.state.copyWith( |
||||
defaultScreen: DefaultScreen.home, |
||||
), |
||||
); |
||||
}, |
||||
icon: const Icon(Icons.home), |
||||
tooltip: 'Set the default to home screen', |
||||
); |
||||
} |
||||
} |
@ -1,3 +0,0 @@ |
||||
class PlatformViewRegistry { |
||||
static void registerViewFactory(String viewId, dynamic cb) {} |
||||
} |